diff --git a/src/main/java/it/inaf/oats/vospace/BaseNodeController.java b/src/main/java/it/inaf/oats/vospace/BaseNodeController.java
index bbc4b403551543f8225bfb7305ac9d567e7eb300..db1bf8d8343b6962e68c28f7534364ec65c999f8 100644
--- a/src/main/java/it/inaf/oats/vospace/BaseNodeController.java
+++ b/src/main/java/it/inaf/oats/vospace/BaseNodeController.java
@@ -16,6 +16,11 @@ public abstract class BaseNodeController {
     private HttpServletRequest servletRequest;
 
     protected String getPath() {
+        // This is to allow calls from the code to CreateNodeController
+        // since request url is not set
+        if(servletRequest.getRequestURL() == null)
+            return null;
+        
         String requestURL = servletRequest.getRequestURL().toString();
         try {
             return NodeUtils.getPathFromRequestURLString(requestURL);
diff --git a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java
index fd867116dee09ed525507e0091e65ab6ac94c672..b8cf12ba3717b278c5726c4ce128916234bcebef 100644
--- a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java
+++ b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java
@@ -36,14 +36,22 @@ public class CreateNodeController extends BaseNodeController {
             consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE},
             produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
     public Node createNode(@RequestBody Node node, User principal) {
-
-        String path = getPath();
-
-        LOG.debug("createNode called for path {}", path);
-
+        
+        LOG.debug("createNode called for node with URI {}", node.getUri());
+        
         // Validate payload node URI
         if (!isValidURI(node.getUri())) {
             throw new InvalidURIException(node.getUri());
+        }        
+        
+        String path;        
+        
+        if(getPath() == null) {
+            LOG.debug("createNode called internally with null path");
+            path = node.getUri().replaceAll("vos://[^/]+", "");
+        } else {            
+            path = getPath();
+            LOG.debug("createNode called for path {}", path);
         }
 
         // Check if payload URI is consistent with http request
diff --git a/src/main/java/it/inaf/oats/vospace/JobService.java b/src/main/java/it/inaf/oats/vospace/JobService.java
index d7eb4c3c38f567fae5b553fe5f73e32aa7778e0a..0b8442ec0ac54b74d3764080020d591ecf94fbee 100644
--- a/src/main/java/it/inaf/oats/vospace/JobService.java
+++ b/src/main/java/it/inaf/oats/vospace/JobService.java
@@ -105,9 +105,8 @@ public class JobService {
                     handleVoSpaceUrlsListResult(job, transfer);
                     break;
                 case moveNode:
-                    throw new UnsupportedOperationException("Not implemented yet");
-                    // handleMoveNode(job, transfer);
-                    // break;
+                    handleMoveNode(transfer);
+                    break;
                 default:
                     throw new UnsupportedOperationException("Not implemented yet");
             }
@@ -147,9 +146,9 @@ public class JobService {
         uriService.setTransferJobResult(job, transfer);
     }
     
-    private void handleMoveNode(JobSummary job, Transfer transfer)
+    private void handleMoveNode(Transfer transfer)
     {
-        moveService.processMoveJob(job, transfer);
+        moveService.processMoveJob(transfer);
     }
 
     private JobDirection getJobDirection(Transfer transfer) {
diff --git a/src/main/java/it/inaf/oats/vospace/MoveService.java b/src/main/java/it/inaf/oats/vospace/MoveService.java
index fbae2fa2ace5f720a032f139cf613deca5cb1167..72708fb220938e75e9074a2318639a3dfdac9628 100644
--- a/src/main/java/it/inaf/oats/vospace/MoveService.java
+++ b/src/main/java/it/inaf/oats/vospace/MoveService.java
@@ -8,66 +8,125 @@ package it.inaf.oats.vospace;
 import it.inaf.ia2.aa.data.User;
 import it.inaf.oats.vospace.datamodel.NodeProperties;
 import it.inaf.oats.vospace.datamodel.NodeUtils;
+import it.inaf.oats.vospace.exception.ContainerNotFoundException;
+import it.inaf.oats.vospace.exception.DuplicateNodeException;
+import it.inaf.oats.vospace.exception.InternalFaultException;
 import it.inaf.oats.vospace.exception.NodeBusyException;
+import it.inaf.oats.vospace.exception.NodeNotFoundException;
 import it.inaf.oats.vospace.exception.PermissionDeniedException;
 import it.inaf.oats.vospace.persistence.NodeDAO;
 import java.util.List;
+import java.util.Optional;
 import javax.servlet.http.HttpServletRequest;
-import net.ivoa.xml.uws.v1.JobSummary;
+import net.ivoa.xml.vospace.v2.ContainerNode;
 import net.ivoa.xml.vospace.v2.Node;
 import net.ivoa.xml.vospace.v2.Transfer;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.transaction.annotation.Transactional;
 
 @Service
+@EnableTransactionManagement
 public class MoveService {
 
     @Autowired
     private NodeDAO nodeDao;
 
+    @Value("${vospace-authority}")
+    private String authority;
+
     @Autowired
-    private HttpServletRequest servletRequest;
+    private CreateNodeController createNodeController;
 
-    public void processMoveJob(JobSummary job, Transfer transfer) {
+    @Autowired
+    private HttpServletRequest servletRequest;
+    
+    @Transactional(rollbackFor = { Exception.class })
+    public void processMoveJob(Transfer transfer) {
 
-        // Get Source Path
-        String sourcePath = transfer.getTarget();
+        // Get Source Vos Path
+        String sourcePath = transfer.getTarget().substring("vos://".length() + authority.length());
 
-        // Get Destination Path (it's in transfer direction)
-        String destinationPath = transfer.getDirection();
+        // Get Destination Vos Path (it's in transfer direction)
+        String destinationPath = transfer.getDirection().substring("vos://".length() + authority.length());
 
         // Extract User permissions from servlet request
         User user = (User) servletRequest.getUserPrincipal();
 
-        Long sourceId = nodeDao.getNodeId(sourcePath);
-        List<Node> branchList = nodeDao.listNodesInBranch(sourceId, true);
-        
-        // Check feasibility of move on source branch
-        if (!isWritePermissionsValid(branchList, user)) {
+        // Generic common validation for move process job paths
+        this.validatePath(sourcePath);
+        this.validatePath(destinationPath);
+
+        // Get source node (this locks it with SELECT ... FOR UPDATE)
+        Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath);
+        if (sourceIdOpt.isEmpty()) {
+            throw new NodeNotFoundException(sourcePath);
+        }
+        Long sourceId = sourceIdOpt.get();
+
+        // Get node branch with root == source. All nodes are locked
+        // with SELECT ... FOR UPDATE
+        List<Node> sourceBranchNodeList = nodeDao.listNodesInBranch(sourceId, true);
+
+        // Check feasibility of move for source branch
+        if (!isWritePermissionsValid(sourceBranchNodeList, user)) {
             throw new PermissionDeniedException(sourcePath);
         }
-        
-        if(sourcePath.equals(destinationPath))
+
+        if (sourcePath.equals(destinationPath)) {
             return;
-        
-        if(!isMoveable(branchList)) {
+        }
+
+        if (!isMoveable(sourceBranchNodeList)) {
             throw new NodeBusyException(sourcePath);
         }
-        
+
         // Set branch at busy        
         nodeDao.setBranchBusy(sourceId, true);
-               
+
+        // EDGE CASE: a node with the same destination path is created by another
+        // process in the database between destination check and move.
+        // This applies also to rename.
+        // the move process would overwrite it or worse create two nodes with
+        // different ids and same vos path
+        // possible solution: check for busyness of parent node when creating
+        // a new node? May it work and be compliant?
         
-        // Compare source and destination paths and see if it's just a rename
-        if(NodeUtils.getParentPath(sourcePath).equals(NodeUtils.getParentPath(destinationPath)))
-        {           
+        // check if destination node exists before
+        if (this.checkNodeExistence(destinationPath)) {
+            throw new DuplicateNodeException(destinationPath);
+        }
+
+        // Compare source and destination paths parents and see if it's just a rename        
+        if (NodeUtils.getParentPath(sourcePath)
+                .equals(NodeUtils.getParentPath(destinationPath))) {
+
             nodeDao.renameNode(sourceId, NodeUtils.getLastPathElement(destinationPath));
+
         } else {
-            this.moveNode(sourceId, sourcePath, destinationPath, user);
+            
+            Long destParentId;
+            
+            Optional<Long> optDest = nodeDao.getNodeId(NodeUtils.getParentPath(destinationPath));
+            if (optDest.isEmpty()) {
+                // Try to create parent container(s)
+                destParentId = this.createDestination(NodeUtils.getParentPath(destinationPath), user);
+            } else {
+                Node parentNode = nodeDao.getNodeById(optDest.get(), true)
+                        .orElseThrow(()->
+                            new NodeNotFoundException(NodeUtils.getParentPath(destinationPath)));
+
+                this.validateDestinationParentNode(parentNode, user);
+                destParentId = optDest.get();
+            }
+
+            this.moveNode(sourceId, destParentId, NodeUtils.getLastPathElement(destinationPath));
         }
 
         nodeDao.setBranchBusy(sourceId, false);
-        
+
     }
 
     // All nodes must be writable by the user to have a true
@@ -81,7 +140,7 @@ public class MoveService {
 
     }
 
-    // All nodes must comply to have a true            
+    // All nodes must comply to have a true output            
     private boolean isMoveable(List<Node> list) {
         return list.stream().allMatch((n) -> {
             boolean busy = NodeUtils.getIsBusy(n);
@@ -93,11 +152,49 @@ public class MoveService {
             return (!busy && !sticky);
         });
     }
-       
+
+    private void moveNode(Long sourceId, Long destParentId, String newNodeName) {
+        nodeDao.moveNodeBranch(sourceId, destParentId);
+        nodeDao.renameNode(sourceId, newNodeName);
+    }
+
+    private void validatePath(String path) {        
+        if (path.equals("/")) {            
+            throw new IllegalArgumentException("Cannot move root node or to root node");
+        }
+    }
+
+    private boolean checkNodeExistence(String path) {
+        Optional<Long> optNodeId = nodeDao.getNodeId(path);
+        return optNodeId.isPresent();
+    }
     
-    private void moveNode(Long sourceId, String sourcePath, String destPath, User user)
-    {
+    // Returns node id of created destination
+    private Long createDestination(String path, User user) {
+        List<String> components = NodeUtils.subPathComponents(path);
+
+        for (int i = 0; i < components.size(); i++) {
+            if (!this.checkNodeExistence(components.get(i))) {
+                ContainerNode node = new ContainerNode();
+                node.setUri("vos://" + this.authority + components.get(i));
+                createNodeController.createNode(node, user);                
+            }
+        }
         
+        return nodeDao.getNodeId(path).orElseThrow(()-> 
+                new InternalFaultException("Unable to create destination at path: "+path));
+            
+    }
+
+    private void validateDestinationParentNode(Node node, User user) {
+        if (!(node instanceof ContainerNode)) {
+            throw new ContainerNotFoundException(
+                    NodeUtils.getVosPath(node));
+        }
+
+        if (!NodeUtils.checkIfWritable(node, user.getName(), user.getGroups())) {
+            throw new PermissionDeniedException(NodeUtils.getVosPath(node));
+        }
     }
 
 }
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
index 4802acc0a4d86b85161d914c50499d8796b1344a..2aaf3937efc56857ade4a6f510399cec5a88514e 100644
--- a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
+++ b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
@@ -46,7 +46,7 @@ public class NodeDAO {
     private String authority;
 
     private final JdbcTemplate jdbcTemplate;
-
+        
     @Autowired
     public NodeDAO(DataSource dataSource) {
         jdbcTemplate = new JdbcTemplate(dataSource);
@@ -231,24 +231,61 @@ public class NodeDAO {
         return node;
     }
 
-    public Long getNodeId(String nodePath) {
+    public Optional<Long> getNodeId(String nodeVosPath) {
         String sql = "SELECT node_id FROM node_vos_path WHERE vos_path = ? FOR UPDATE";
 
         List<Long> nodeIdList = jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
-            ps.setString(1, nodePath);
+            ps.setString(1, nodeVosPath);
             return ps;
         }, (row, index) -> {
             return row.getLong("node_id");
         });
 
-        // Node id is 
-        if (nodeIdList.isEmpty()) {
-            throw new NodeNotFoundException(nodePath);
+        switch (nodeIdList.size()) {
+            case 0:
+                return Optional.empty();
+
+            case 1:
+                return Optional.of(nodeIdList.get(0));
+
+            default:
+                throw new InternalFaultException("More than 1 node id at path: " + nodeVosPath);
         }
+    }  
+
+    public Optional<Node> getNodeById(Long nodeId, boolean enforceTapeStoredCheck) {
+        String sql = "SELECT os.vos_path, loc.location_type, n.node_id, type, async_trans, sticky, busy_state, creator_id, group_read, group_write,\n"
+                + "is_public, content_length, created_on, last_modified, accept_views, provide_views\n"
+                + "FROM node n\n"
+                + "JOIN node_vos_path os ON n.node_id = os.node_id\n"
+                + "JOIN location loc ON n.location_id = loc.location_id\n"
+                + "WHERE n.node_id = ?\n"
+                + "FOR UPDATE";
+
+        List<Node> result = jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setLong(1, nodeId);
+            return ps;
+        }, (row, index) -> {
+            if (enforceTapeStoredCheck && row.getString("location_type").equals("async")) {
+                throw new InternalFaultException(
+                        "Node id: " + nodeId + " has async location type. "
+                        + "Failure due to enforced check.");
+            }
+            return getNodeFromResultSet(row);
+        });
+
+        switch (result.size()) {
+            case 0:
+                return Optional.empty();
 
-        // Node id is PRIMARY KEY: uniqueness is enforced at DB level
-        return nodeIdList.get(0);
+            case 1:
+                return Optional.of(result.get(0));
+
+            default:
+                throw new InternalFaultException("Multiple nodes with id: " + nodeId);
+        }
     }
 
     // First node is the root node 
@@ -268,7 +305,8 @@ public class NodeDAO {
         }, (row, index) -> {
             if (enforceTapeStoredCheck && row.getString("location_type").equals("async")) {
                 throw new InternalFaultException(
-                        "At least one node in branch has async location type. "
+                        "At least one node in branch with root id: " + rootNodeId
+                        + " has async location type. "
                         + "Failure due to enforced check.");
             }
             return getNodeFromResultSet(row);
@@ -302,6 +340,72 @@ public class NodeDAO {
         });
 
     }
+    
+    /*
+    // unused?
+    public Optional<String> getNodeLtreePathById(Long nodeId) {
+        String sql = "SELECT path FROM node WHERE node_id = ? FOR UPDATE";
+
+        List<String> pathList = jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setLong(1, nodeId);
+            return ps;
+        }, (row, index) -> {
+            return row.getString("path");
+        });
+
+        switch (pathList.size()) {
+            case 0:
+                return Optional.empty();
+
+            case 1:
+                return Optional.of(pathList.get(0));
+
+            default:
+                throw new InternalFaultException("More than one id = " + nodeId);
+        }
+    }
+    
+    //remove
+    public String getParentPath(Long id) {
+        String sql = "SELECT parent_path FROM node WHERE node_id = ?";
+        
+        List<String> nodeIdList = jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setLong(1, id);
+            return ps;
+        }, (row, index) -> {
+            return row.getString("parent_path");
+        });
+        
+        if(nodeIdList.size() > 0)
+        {
+            return nodeIdList.get(0);            
+        } else {
+            return null;
+        }
+            
+        
+    }
+    */
+    
+    public void moveNodeBranch(Long sourceRootId, Long destinationParentId)
+    {
+        String sql = "UPDATE node\n"+ 
+                "SET parent_path = ((SELECT path FROM node WHERE node_id = ?) ||\n"+
+                "(CASE WHEN node_id = ? THEN '' ELSE subpath(parent_path, index(parent_path,(?::varchar)::ltree)) END))\n" +
+                "WHERE path ~ ('*.' || ? || '.*')::lquery";
+        
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setLong(1, destinationParentId);
+            ps.setLong(2, sourceRootId);
+            ps.setLong(3, sourceRootId);            
+            ps.setLong(4, sourceRootId);
+            return ps;
+        });
+                
+    }
 
     public void deleteNode(String path) {
         int nodesWithPath = countNodesWithPath(path);
diff --git a/src/test/java/it/inaf/oats/vospace/MoveServiceTest.java b/src/test/java/it/inaf/oats/vospace/MoveServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..155bc1004103b6fc7c3536c39c4fd3c9ec59e9c4
--- /dev/null
+++ b/src/test/java/it/inaf/oats/vospace/MoveServiceTest.java
@@ -0,0 +1,296 @@
+/*
+ * This file is part of vospace-rest
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.oats.vospace;
+
+import it.inaf.ia2.aa.data.User;
+import it.inaf.oats.vospace.datamodel.NodeProperties;
+import it.inaf.oats.vospace.exception.DuplicateNodeException;
+import it.inaf.oats.vospace.exception.InternalFaultException;
+import it.inaf.oats.vospace.exception.NodeBusyException;
+import it.inaf.oats.vospace.exception.NodeNotFoundException;
+import it.inaf.oats.vospace.exception.PermissionDeniedException;
+import it.inaf.oats.vospace.persistence.DataSourceConfigSingleton;
+import it.inaf.oats.vospace.persistence.NodeDAO;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.vospace.v2.ContainerNode;
+import net.ivoa.xml.vospace.v2.Property;
+import net.ivoa.xml.vospace.v2.Transfer;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.ContextConfiguration;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@ContextConfiguration(classes = {DataSourceConfigSingleton.class, MoveServiceTest.TestConfig.class})
+@TestPropertySource(locations = "classpath:test.properties", properties = {"vospace-authority=example.com!vospace", "file-service-url=http://file-service"})
+@TestMethodOrder(OrderAnnotation.class)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
+public class MoveServiceTest {
+
+    @Value("${vospace-authority}")
+    private String authority;
+    
+    @Autowired
+    private MoveService moveService;
+    
+    @Autowired
+    private NodeDAO nodeDao;
+
+    @Autowired
+    private HttpServletRequest servletRequest;
+       
+    @TestConfiguration
+    public static class TestConfig {
+
+        /**
+         * Necessary because MockBean doesn't work with HttpServletRequest.
+         */
+        @Bean
+        @Primary
+        public HttpServletRequest servletRequest() {
+            HttpServletRequest request = mock(HttpServletRequest.class);
+            User user = new User().setUserId("anonymous");
+            when(request.getUserPrincipal()).thenReturn(user);
+            return request;
+        }
+    }
+    
+    @Test
+    @Order(1)
+    public void moveRootTest() {
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            moveService.processMoveJob(getTransfer("/", "/pippo"));
+        }
+        );
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            moveService.processMoveJob(getTransfer("/pippo", "/"));
+        }
+        );
+
+    }
+    
+    @Test
+    @Order(2)
+    public void testNonExistingSourceNode() {        
+        assertThrows(NodeNotFoundException.class, () -> {
+            moveService.processMoveJob(getTransfer("/pippo", "/test2"));
+        }
+        );        
+    }  
+    
+    @Test
+    @Order(3)
+    public void testMoveDeniedOnAsync() {        
+        assertThrows(InternalFaultException.class, () -> {
+            moveService.processMoveJob(getTransfer("/test1", "/test4"));
+        }
+        );        
+    }
+    
+    @Test
+    @Order(4)
+    public void testPermissionDenied() {
+        User user = mock(User.class);        
+        when(user.getName()).thenReturn("user1");
+        when(servletRequest.getUserPrincipal()).thenReturn(user);
+        
+        assertThrows(PermissionDeniedException.class, () -> {
+            moveService.processMoveJob(getTransfer("/test3/m1", "/test4"));
+        }
+        );        
+    }    
+    
+    @Test
+    @Order(5)
+    public void testDestinationNodeAlreadyExisting() {
+        User user = mock(User.class);        
+        when(user.getName()).thenReturn("user3");
+        when(servletRequest.getUserPrincipal()).thenReturn(user);        
+        
+        assertThrows(DuplicateNodeException.class, () -> {
+            moveService.processMoveJob(getTransfer("/test3/m1", "/test4"));
+        }
+        );        
+    }
+    
+    @Test
+    @Order(6)
+    public void testBusyNodeInSourceBranch() {
+        User user = mock(User.class);        
+        when(user.getName()).thenReturn("user3");
+        when(servletRequest.getUserPrincipal()).thenReturn(user);
+        
+        nodeDao.setBranchBusy(nodeDao.getNodeId("/test3/m1/m2").orElseThrow(), true);
+        
+        assertThrows(NodeBusyException.class, () -> {
+            moveService.processMoveJob(getTransfer("/test3/m1", "/test4"));
+        }
+        );
+                
+        nodeDao.setBranchBusy(nodeDao.getNodeId("/test3/m1/m2").orElseThrow(), false);        
+    }
+    
+    @Test
+    @Order(7)
+    public void testNoMoveOnSticky() {
+        User user = mock(User.class);        
+        when(user.getName()).thenReturn("user3");
+        when(servletRequest.getUserPrincipal()).thenReturn(user);
+        
+        assertThrows(NodeBusyException.class, () -> {
+            moveService.processMoveJob(getTransfer("/test3/mstick", "/test4"));
+        }
+        );
+        
+    }
+    
+    @Test
+    @Order(8)
+    public void testRenameNode() {
+        User user = mock(User.class);        
+        when(user.getName()).thenReturn("user3");
+        when(servletRequest.getUserPrincipal()).thenReturn(user);
+        
+        Optional<Long> sourceId = nodeDao.getNodeId("/test3/m1");
+        assertTrue(sourceId.isPresent());
+        Optional<Long> childId = nodeDao.getNodeId("/test3/m1/m2");
+        assertTrue(childId.isPresent());
+        // Rename
+        moveService.processMoveJob(getTransfer("/test3/m1", "/test3/m1ren"));
+        
+        Optional<Long> checkSourceId = nodeDao.getNodeId("/test3/m1");
+        assertTrue(checkSourceId.isEmpty());
+        
+        Optional<Long> newSourceId = nodeDao.getNodeId("/test3/m1ren");
+        assertTrue(newSourceId.isPresent());
+        assertEquals(sourceId.get(), newSourceId.get());
+        
+        Optional<Long> newChildId = nodeDao.getNodeId("/test3/m1ren/m2");
+        assertTrue(newChildId.isPresent());
+        assertEquals(childId.get(), newChildId.get());        
+        
+    }
+    
+    @Test
+    @Order(9)
+    public void testMoveToExistingParent(){
+        User user = mock(User.class);        
+        when(user.getName()).thenReturn("user3");
+        when(servletRequest.getUserPrincipal()).thenReturn(user);
+        
+        // Preliminary checks for assumptions
+        Optional<Long> sourceId = nodeDao.getNodeId("/test3/m1");
+        assertTrue(sourceId.isPresent());
+        Optional<Long> childId = nodeDao.getNodeId("/test3/m1/m2");
+        assertTrue(childId.isPresent());
+        
+        Optional<Long> destParentId = nodeDao.getNodeId("/test4");
+        assertTrue(destParentId.isPresent());
+        
+        Optional<Long> destId = nodeDao.getNodeId("/test4/dest1");
+        assertTrue(destId.isEmpty());
+        
+        // move
+        moveService.processMoveJob(getTransfer("/test3/m1", "/test4/dest1"));
+
+        // source has been moved
+        Optional<Long> oldSourceId = nodeDao.getNodeId("/test3/m1");
+        assertTrue(oldSourceId.isEmpty());
+        Optional<Long> oldChildId = nodeDao.getNodeId("/test3/m1/m2");
+        assertTrue(oldChildId.isEmpty());
+        
+        Optional<Long> newSourceId = nodeDao.getNodeId("/test4/dest1");
+        assertTrue(newSourceId.isPresent());
+        assertEquals(sourceId.get(), newSourceId.get());
+        
+        Optional<Long> newChildId = nodeDao.getNodeId("/test4/dest1/m2");
+        assertTrue(newChildId.isPresent());
+        assertEquals(childId.get(), newChildId.get());
+        
+    }
+    
+    @Test
+    @Order(10)
+    public void testMoveToUnexistingParent() {
+        User user = mock(User.class);        
+        when(user.getName()).thenReturn("user3");
+        when(servletRequest.getUserPrincipal()).thenReturn(user);
+        
+        Optional<Long> sourceId = nodeDao.getNodeId("/test3/m1");
+        assertTrue(sourceId.isPresent());
+        Optional<Long> childId = nodeDao.getNodeId("/test3/m1/m2");
+        assertTrue(childId.isPresent());
+        
+        Optional<Long> destParentId = nodeDao.getNodeId("/test4");
+        assertTrue(destParentId.isPresent());
+        
+        Optional<Long> destCreatemeId = nodeDao.getNodeId("/test4/createme");
+        assertTrue(destCreatemeId.isEmpty());
+        
+        // Rename
+        moveService.processMoveJob(getTransfer("/test3/m1", "/test4/createme/dest1"));
+        
+        Optional<Long> checkSourceId = nodeDao.getNodeId("/test3/m1");
+        assertTrue(checkSourceId.isEmpty());
+        
+        Optional<Long> newCreatemeId = nodeDao.getNodeId("/test4/createme");
+        assertTrue(newCreatemeId.isPresent());        
+        
+        Optional<Long> newSourceId = nodeDao.getNodeId("/test4/createme/dest1");
+        assertTrue(newSourceId.isPresent());
+        assertEquals(sourceId.get(), newSourceId.get());
+
+        Optional<Long> newChildId = nodeDao.getNodeId("/test4/createme/dest1/m2");
+        assertTrue(newChildId.isPresent());
+        assertEquals(childId.get(), newChildId.get());        
+        
+    }     
+
+    private Transfer getTransfer(String vosTarget, String vosDestination) {
+        Transfer transfer = new Transfer();
+        transfer.setTarget("vos://" + this.authority + vosTarget);
+        transfer.setDirection("vos://" + this.authority + vosDestination);
+        return transfer;
+    }
+    
+    private ContainerNode getContainerNode(String vosPath, String owner, String writeGroups)
+    {
+        ContainerNode node = new ContainerNode();
+        node.setUri("vos://"+this.authority+vosPath);
+        Property ownerProp = new Property();
+        ownerProp.setUri(NodeProperties.CREATOR_URI);
+        ownerProp.setValue(owner);
+        
+        Property writeGroupsProp = new Property();
+        writeGroupsProp.setUri(NodeProperties.GROUP_WRITE_URI);
+        writeGroupsProp.setValue(writeGroups);
+        
+        node.getProperties().add(ownerProp);
+        node.getProperties().add(writeGroupsProp);
+        
+        return node;
+    }
+
+}
diff --git a/src/test/java/it/inaf/oats/vospace/persistence/DataSourceConfigSingleton.java b/src/test/java/it/inaf/oats/vospace/persistence/DataSourceConfigSingleton.java
new file mode 100644
index 0000000000000000000000000000000000000000..2430defaaa04f651ad4286d3d005fafe7d2b897a
--- /dev/null
+++ b/src/test/java/it/inaf/oats/vospace/persistence/DataSourceConfigSingleton.java
@@ -0,0 +1,122 @@
+/*
+ * This file is part of vospace-rest
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.oats.vospace.persistence;
+
+import com.opentable.db.postgres.embedded.EmbeddedPostgres;
+import com.opentable.db.postgres.embedded.PgBinaryResolver;
+import com.opentable.db.postgres.embedded.UncompressBundleDirectoryResolver;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.sql.DataSource;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.datasource.init.ScriptUtils;
+
+/**
+ * Generates a DataSource that can be used for testing DAO classes. It loads an
+ * embedded Postgres database and fills it using the data from
+ * vospace-transfer-service repository (folder must exists; it location can be
+ * configured using the init_database_scripts_path in test.properties).
+ */
+@TestConfiguration
+public class DataSourceConfigSingleton {
+
+    @Value("${init_database_scripts_path}")
+    private String scriptPath;
+
+    @Bean    
+    @Primary
+    public DataSource dataSource() throws Exception {
+        DataSource embeddedPostgresDS = EmbeddedPostgres.builder()
+                .setPgDirectoryResolver(new UncompressBundleDirectoryResolver(new CustomPostgresBinaryResolver()))
+                .start().getPostgresDatabase();
+
+        initDatabase(embeddedPostgresDS);
+
+        return embeddedPostgresDS;
+    }
+
+    private class CustomPostgresBinaryResolver implements PgBinaryResolver {
+
+        /**
+         * Loads specific embedded Postgres version.
+         */
+        @Override
+        public InputStream getPgBinary(String system, String architecture) throws IOException {
+            ClassPathResource resource = new ClassPathResource(String.format("postgres-%s-%s.txz", system.toLowerCase(), architecture));
+            return resource.getInputStream();
+        }
+    }
+
+    /**
+     * Loads SQL scripts for database initialization from
+     * vospace-transfer-service repo directory.
+     */
+    private void initDatabase(DataSource dataSource) throws Exception {
+        try ( Connection conn = dataSource.getConnection()) {
+
+            File currentDir = new File(DataSourceConfigSingleton.class.getClassLoader().getResource(".").getFile());
+            File scriptDir = currentDir.toPath().resolve(scriptPath).toFile().getCanonicalFile();
+
+            assertTrue(scriptDir.exists(), "DAO tests require " + scriptDir.getAbsolutePath() + " to exists.\n"
+                    + "Please clone the repository from https://www.ict.inaf.it/gitlab/vospace/vospace-file-catalog.git");
+
+            File[] scripts = scriptDir.listFiles(f -> f.getName().endsWith(".sql"));
+            Arrays.sort(scripts); // sort alphabetically
+
+            for (File script : scripts) {
+                ByteArrayResource scriptResource = replaceDollarQuoting(script.toPath());
+                ScriptUtils.executeSqlScript(conn, scriptResource);
+            }
+
+            ScriptUtils.executeSqlScript(conn, new ClassPathResource("test-data.sql"));
+        }
+    }
+
+    /**
+     * It seems that dollar quoting (used in UDF) is broken in JDBC. Replacing
+     * it with single quotes solves the problem. We replace the quoting here
+     * instead of inside the original files because dollar quoting provides a
+     * better visibility.
+     */
+    private ByteArrayResource replaceDollarQuoting(Path sqlScriptPath) throws Exception {
+
+        String scriptContent = Files.readString(sqlScriptPath);
+
+        if (scriptContent.contains("$func$")) {
+
+            String func = extractFunctionDefinition(scriptContent);
+
+            String originalFunction = "$func$" + func + "$func$";
+            String newFunction = "'" + func.replaceAll("'", "''") + "'";
+
+            scriptContent = scriptContent.replace(originalFunction, newFunction);
+        }
+
+        return new ByteArrayResource(scriptContent.getBytes());
+    }
+
+    private String extractFunctionDefinition(String scriptContent) {
+        Pattern pattern = Pattern.compile("\\$func\\$(.*?)\\$func\\$", Pattern.DOTALL);
+        Matcher matcher = pattern.matcher(scriptContent);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        throw new IllegalArgumentException(scriptContent + " doesn't contain $func$");
+    }
+}
diff --git a/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java b/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java
index 501ca5455d4411c8de0aab7f57a613b8fb2cf4ec..31eedd2efc8e7c603e5611d61a848dbce68923fa 100644
--- a/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java
+++ b/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java
@@ -8,11 +8,11 @@ package it.inaf.oats.vospace.persistence;
 import it.inaf.oats.vospace.datamodel.NodeProperties;
 import it.inaf.oats.vospace.datamodel.NodeUtils;
 import it.inaf.oats.vospace.exception.InternalFaultException;
-import it.inaf.oats.vospace.exception.NodeNotFoundException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import javax.sql.DataSource;
 import net.ivoa.xml.vospace.v2.ContainerNode;
@@ -21,7 +21,6 @@ import net.ivoa.xml.vospace.v2.Node;
 import net.ivoa.xml.vospace.v2.Property;
 import net.ivoa.xml.vospace.v2.View;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import org.junit.jupiter.api.BeforeEach;
@@ -66,7 +65,7 @@ public class NodeDAOTest {
     @Test
     public void testListNode() {
         ContainerNode root = (ContainerNode) dao.listNode("/").get();
-        assertEquals(2, root.getNodes().size());
+        assertEquals(4, root.getNodes().size());
 
         assertEquals("true", NodeProperties.getNodePropertyByURI(root, NodeProperties.PUBLIC_READ_URI));
         assertEquals("0", NodeProperties.getNodePropertyByURI(root, NodeProperties.LENGTH_URI));
@@ -80,18 +79,40 @@ public class NodeDAOTest {
 
     @Test
     public void testGetNodeId() {
-        assertEquals(2, dao.getNodeId("/test1"));
-        assertEquals(3, dao.getNodeId("/test1/f1"));
-
-        assertThrows(NodeNotFoundException.class,
+        Optional<Long> id1 = dao.getNodeId("/test1");
+        assertTrue(id1.isPresent());        
+        assertEquals(2, id1.get());
+        
+        Optional<Long> id2 = dao.getNodeId("/test1/f1");
+        assertTrue(id2.isPresent());
+        assertEquals(3, id2.get());
+        
+        Optional<Long> id3 = dao.getNodeId("/pippo123123");
+        assertTrue(id3.isEmpty());
+    }
+    
+    @Test
+    public void testGetNodeById() {
+        Optional<Long> id1 = dao.getNodeId("/test1/f1");
+        assertTrue(id1.isPresent());
+        
+        assertThrows(InternalFaultException.class,
                 () -> {
-                    dao.getNodeId("/pippo123123");
+                    dao.getNodeById(id1.get(), true);
                 });
-    }
+        
+        Optional<Node> opt1 = dao.getNodeById(id1.get(), false);
+        
+        assertTrue(opt1.isPresent());
+        assertTrue(NodeUtils.getVosPath(opt1.get()).equals("/test1/f1"));
+    }    
 
     @Test
     public void testListNodesInBranch() {
-        List<Node> result = dao.listNodesInBranch(dao.getNodeId("/test1/f1"), false);
+        Optional<Long> id1 = dao.getNodeId("/test1/f1");
+        assertTrue(id1.isPresent());
+        
+        List<Node> result = dao.listNodesInBranch(id1.get(), false);
         assertEquals(3, result.size());
         // Check if list has root node at index 0
         Node root = result.get(0);
@@ -99,16 +120,18 @@ public class NodeDAOTest {
 
         assertThrows(InternalFaultException.class,
                 () -> {
-                    dao.listNodesInBranch(dao.getNodeId("/test1/f1"), true);
+                    dao.listNodesInBranch(id1.get(), true);
                 });
 
     }
 
     @Test
     public void testSetBranchBusy() {
-        Long rootId = dao.getNodeId("/test1/f1");
-        dao.setBranchBusy(rootId, true);
-        List<Node> busyList = dao.listNodesInBranch(rootId, false);
+        Optional<Long> rootId = dao.getNodeId("/test1/f1");
+        assertTrue(rootId.isPresent());
+        
+        dao.setBranchBusy(rootId.get(), true);
+        List<Node> busyList = dao.listNodesInBranch(rootId.get(), false);
         boolean busyTrue = busyList.stream().allMatch((n) -> {
             if (n instanceof DataNode) {
                 return ((DataNode) n).isBusy();
@@ -120,9 +143,9 @@ public class NodeDAOTest {
 
         assertTrue(busyTrue);
 
-        dao.setBranchBusy(rootId, false);
+        dao.setBranchBusy(rootId.get(), false);
 
-        busyList = dao.listNodesInBranch(rootId, false);
+        busyList = dao.listNodesInBranch(rootId.get(), false);
 
         boolean busyFalse = busyList.stream().allMatch((n) -> {
             if (n instanceof DataNode) {
@@ -146,9 +169,10 @@ public class NodeDAOTest {
 
         assertTrue(dao.listNode(oldPath).isPresent());
         assertTrue(dao.listNode(oldPathChild).isPresent());
-        Long rootId = dao.getNodeId(oldPath);
+        Optional<Long> rootId = dao.getNodeId(oldPath);
+        assertTrue(rootId.isPresent());
 
-        dao.renameNode(rootId, "f_pippo");
+        dao.renameNode(rootId.get(), "f_pippo");
 
         assertTrue(dao.listNode(oldPath).isEmpty());
         assertTrue(dao.listNode(oldPathChild).isEmpty());
@@ -157,6 +181,39 @@ public class NodeDAOTest {
         assertTrue(dao.listNode(newPathChild).isPresent());
 
     }
+    
+    @Test
+    public void testMoveNodeBranch() {
+        Optional<Long> optSourceId = dao.getNodeId("/test1/f1");
+        assertTrue(optSourceId.isPresent());        
+        
+        Optional<Long> optSourceId1 = dao.getNodeId("/test1/f1/f2_renamed");
+        assertTrue(optSourceId1.isPresent());
+        
+        Optional<Long> optSourceId2 = dao.getNodeId("/test1/f1/f2_renamed/f3");
+        assertTrue(optSourceId2.isPresent());
+        
+        Optional<Long> optDestId = dao.getNodeId("/test2/f4");
+        assertTrue(optDestId.isPresent());
+        
+        dao.moveNodeBranch(optSourceId.get(), optDestId.get());
+
+        Optional<Long> newOptSourceId = dao.getNodeId("/test1/f1");
+        assertTrue(newOptSourceId.isEmpty());
+                       
+        Optional<Long> dest = dao.getNodeId("/test2/f4/f1");
+        assertTrue(dest.isPresent());
+        assertEquals(dest.get(), optSourceId.get());
+        
+        Optional<Long> dest1 = dao.getNodeId("/test2/f4/f1/f2_renamed");        
+        assertTrue(dest1.isPresent());
+        assertEquals(dest1.get(), optSourceId1.get());
+        
+        Optional<Long> dest2 = dao.getNodeId("/test2/f4/f1/f2_renamed/f3");
+        assertTrue(dest2.isPresent());
+        assertEquals(dest2.get(), optSourceId2.get());
+        
+    }
 
     @Test
     public void testCountNodeWithPath() {
diff --git a/src/test/resources/test-data.sql b/src/test/resources/test-data.sql
index e31766b9478c847556509ba60f5feafb1b471da3..adf89ffc32789671643f0ee3dff4190533043f90 100644
--- a/src/test/resources/test-data.sql
+++ b/src/test/resources/test-data.sql
@@ -23,6 +23,14 @@ INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('6', '', 'f4', 'container', 'user2', true, 1);    -- /test2/f4 (rel: /f4)
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('6', '', 'f5', 'container', 'user2', true, 1);    -- /test2/f5 (rel: /f5)
 
+INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('', NULL, 'test3', 'container', 'user3', false, 3);      -- /test3
+INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('9', '', 'm1', 'container', 'user3', false, 3);      -- /test3/m1
+INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('9.10', '', 'm2', 'container', 'user3', false, 3);      -- /test3/m1/m2
+
+INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('', NULL, 'test4', 'container', 'user3', false, 3);      -- /test4
+
+INSERT INTO node (parent_path, parent_relative_path, name, sticky, type, creator_id, is_public, location_id) VALUES ('9', '', 'mstick', true, 'container', 'user3', false, 3);      -- /test3/mstick
+
 DELETE FROM job;
 
 INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo1', 'user1', 'pullFromVoSpace', 'ARCHIVED', NULL, NULL, '2011-06-22 19:10:25', NULL, NULL);