diff --git a/vospace-ui-backend/pom.xml b/vospace-ui-backend/pom.xml index f79424232d75ac72192efcb820df92adc35b543c..7fc3e0739aed1425547f6649dad30c9d53e3675c 100644 --- a/vospace-ui-backend/pom.xml +++ b/vospace-ui-backend/pom.xml @@ -106,6 +106,9 @@ <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> + <configuration> + <trimStackTrace>false</trimStackTrace> + </configuration> </plugin> <plugin> <groupId>org.jacoco</groupId> diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java index 819a285d6581ecc29b9433edc6223ee5a7ae24d4..c4a3659c492987f2e44d702f561f1fb2ecea9883 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -142,7 +142,7 @@ public class VOSpaceClient { String path = node.getUri().substring(("vos://" + authority).length()); - HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) + HttpRequest request = getRequest("/nodes" + path) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) @@ -166,7 +166,7 @@ public class VOSpaceClient { String path = node.getUri().substring(("vos://" + authority).length()); - HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive) + HttpRequest request = getRequest("/nodes" + path + "?recursive=" + recursive) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java index fe5ffa615e2ee1ed784bad177b0a3b31e736a97e..5413860675d77eeceb1690e72448c0f0f51b8637 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -13,6 +13,7 @@ import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveNodeModalHtmlGenerator; import it.inaf.oats.vospace.datamodel.NodeUtils; +import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -79,6 +80,8 @@ public class NodesController extends BaseController { @GetMapping(value = "/nodesForMove", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<ListNodeData> listNodesForMoveModal(@RequestParam("path") String path, @RequestParam("nodeToMove") String nodeToMove, User principal) throws Exception { + LOG.debug("listNodes called for path {}", path); + ListNodeData listNodeData = new ListNodeData(); Node node = client.getNode(path); @@ -99,7 +102,7 @@ public class NodesController extends BaseController { Transfer transfer = new Transfer(); transfer.setDirection("pullFromVoSpace"); - transfer.setTarget(Arrays.asList("vos://" + authority + path)); + transfer.setTarget(Arrays.asList("vos://" + authority + urlEncodePath(path))); Protocol protocol = new Protocol(); protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); @@ -123,7 +126,7 @@ public class NodesController extends BaseController { LOG.debug("newFolder called for path {}/{}", parentPath, name); ContainerNode node = new ContainerNode(); - node.setUri("vos://" + authority + parentPath + "/" + name); + node.setUri("vos://" + authority + urlEncodePath(parentPath + "/" + name)); Property creator = new Property(); creator.setUri("ivo://ivoa.net/vospace/core#creator"); @@ -162,8 +165,8 @@ public class NodesController extends BaseController { @PostMapping(value = "/move") public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) { - String target = getRequiredParam(params, "target"); - String direction = getRequiredParam(params, "direction"); + String target = urlEncodePath(getRequiredParam(params, "target")); + String direction = urlEncodePath(getRequiredParam(params, "direction")); Transfer transfer = new Transfer(); transfer.setTarget(Arrays.asList("vos://" + authority + target)); diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java index 3508f3f9d598ac85b36ff105bd767d2ee3e935c9..a77fc8202f9bff8fe70a126b5ec71ea3e8e1f975 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java @@ -8,6 +8,7 @@ package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.UploadFilesData; import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException; +import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -64,11 +65,15 @@ public class UploadController extends BaseController { public CompletableFuture<String> prepareForDownload(String parentPath, String fileName) { return CompletableFuture.supplyAsync(() -> { - String nodeUri = "vos://" + authority + parentPath; - if (!nodeUri.endsWith("/")) { - nodeUri += "/"; + + String path = parentPath; + + if (!path.endsWith("/")) { + path += "/"; } - nodeUri += fileName; + path += fileName; + + String nodeUri = "vos://" + authority + urlEncodePath(path); createDataNode(nodeUri, getUser().getName()); diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java index 68e51080d63d0b0a588dd6d216fe56516a28cdf6..5b3d6154514a8f75bc51aecaf5b947be116762a3 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java @@ -9,12 +9,14 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Optional; +import java.util.stream.Collectors; import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Property; -import net.ivoa.xml.vospace.v2.View; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +43,7 @@ public class NodeInfo { public NodeInfo(Node node, User user, String authority) { this.authority = authority; this.path = getPath(node); - this.name = path.substring(path.lastIndexOf("/") + 1); + this.name = URLDecoder.decode(path.substring(path.lastIndexOf("/") + 1), StandardCharsets.UTF_8); this.size = getSize(node); this.type = node.getType(); this.creator = getCreator(node); @@ -65,7 +67,10 @@ public class NodeInfo { throw new VOSpaceException("Node authority is different from configured one! Configured is " + authority + ", but node URI is " + uri); } - return uri.substring(prefix.length()); + // returns decoded path + return String.join("/", Arrays.stream(uri.substring(prefix.length()).split("/")) + .map(p -> URLDecoder.decode(p, StandardCharsets.UTF_8)) + .collect(Collectors.toList())); } private String getCreator(Node node) { diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java index a816220c99f38691df72680e8f7f62855a502d59..2fa498dfd106d8560f012153d44cf52dbc9e48f7 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java @@ -10,6 +10,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.http.HttpResponse; @@ -24,6 +25,8 @@ import net.ivoa.xml.vospace.v2.Protocol; 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 static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -90,17 +93,17 @@ public class VOSpaceClientTest { } @Test - public void testCreateNodeBadName() { + public void testCreateNodeBadUri() { ContainerNode newNode = new ContainerNode(); - newNode.setUri("vos://ia2.inaf.it!vospace/mynode/File with spaces.and.dots.pdf"); + newNode.setUri("vos://ia2.inaf.it!vospace/mynode/spaces not encoded"); - ReflectionTestUtils.setField(voSpaceClient, "useJson", false); - - CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("node-response.xml")); - when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); - - voSpaceClient.createNode(newNode); + try { + voSpaceClient.createNode(newNode); + fail("Exception was expected"); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getCause() instanceof URISyntaxException); + } } @Test @@ -195,6 +198,41 @@ public class VOSpaceClientTest { assertEquals(ExecutionPhase.COMPLETED, voSpaceClient.getJobPhase("job_id")); } + + @Test + public void testSetNode() { + + ContainerNode node = new ContainerNode(); + node.setUri("vos://ia2.inaf.it!vospace/mynode"); + + CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("node-response.xml")); + when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); + + voSpaceClient.setNode(node, true); + } + + @Test + public void testSetNodeBadUri() { + + ContainerNode node = new ContainerNode(); + node.setUri("vos://ia2.inaf.it!vospace/not urlencoded"); + + try { + voSpaceClient.setNode(node, true); + fail("Exception was expected"); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getCause() instanceof URISyntaxException); + } + } + + @Test + public void testDeleteNode() { + + CompletableFuture response = getMockedStreamResponseFuture(200, ""); + when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); + + voSpaceClient.deleteNode("/not urlencoded"); + } protected static String getResourceFileContent(String fileName) { try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) { diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java index 5879848a56e21417273b28c52012903d74f2526c..013590ded3a774ae6ade34d97ff1db56f3e495b8 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java @@ -6,12 +6,14 @@ package it.inaf.ia2.vospace.ui.controller; import com.fasterxml.jackson.databind.ObjectMapper; +import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import java.util.Arrays; import java.util.List; import java.util.Map; +import javax.servlet.http.HttpSession; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; @@ -24,6 +26,7 @@ import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,6 +36,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -50,6 +54,9 @@ public class NodesControllerTest { @MockBean private VOSpaceClient client; + @Autowired + private NodesController nodesController; + @Autowired private MockMvc mockMvc; @@ -198,6 +205,33 @@ public class NodesControllerTest { } } + @Test + public void testDirectDownload() throws Exception { + + when(client.getFileServiceEndpoint(any())).thenReturn("http://redirect"); + + mockMvc.perform(get("/download/myfile")) + .andExpect(status().is3xxRedirection()); + + verify(client, times(1)).getFileServiceEndpoint(any()); + } + + @Test + public void testNewFolder() throws Exception { + + HttpSession mockedSession = mock(HttpSession.class); + ReflectionTestUtils.setField(nodesController, "session", mockedSession); + + when(mockedSession.getAttribute("user_data")).thenReturn(mock(User.class)); + + mockMvc.perform(post("/folder") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"parentPath\": \"/parent\", \"name\": \"newFolder\"}")) + .andExpect(status().is2xxSuccessful()); + + verify(client, times(1)).createNode(any()); + } + private ResultActions testMoveNode() throws Exception { JobSummary job = new JobSummary(); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java index 9ed64e0555aa3e657a97729068ad1b7511038aca..dacb50001f1da687d2e5f4722d7e31f83fa50723 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java @@ -50,6 +50,14 @@ public class NodeInfoTest { public void testSizePeta() { testNodeLength(6963696737150000L, "6.2 PB"); } + + @Test + public void testUrlEncoding() { + DataNode node = new DataNode(); + node.setUri("vos://example.com!vospace/my%23node"); + NodeInfo nodeInfo = new NodeInfo(node, new User(), AUTHORITY); + assertEquals("my#node", nodeInfo.getName()); + } private void testNodeLength(long bytes, String expectedText) { DataNode node = getDataNode();