diff --git a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java index 5c124efe3909508a4df3d11c919d3245a061ad6d..759af9ffa271a04cb383b43594eb320622fdbcbd 100644 --- a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java +++ b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java @@ -6,6 +6,9 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; +import it.inaf.oats.vospace.exception.InvalidArgumentException; +import it.inaf.oats.vospace.exception.InvalidURIException; +import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.Node; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; @@ -14,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PutMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; @RestController public class CreateNodeController extends BaseNodeController { @@ -23,6 +27,9 @@ public class CreateNodeController extends BaseNodeController { @Autowired private CreateNodeService createNodeService; + @Value("${vospace-authority}") + private String authority; + @PutMapping(value = {"/nodes", "/nodes/**"}, 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}) @@ -32,8 +39,36 @@ public class CreateNodeController extends BaseNodeController { LOG.debug("createNodeController called for node with URI {} and PATH {}", node.getUri(), path); + // Get Node path (and validates it too) + String decodedURIPathFromNode = URIUtils.returnVosPathFromNodeURI(node.getUri(), authority); + + LOG.debug("createNodeController URI: {} decoded as {}", node.getUri(), decodedURIPathFromNode); + + // Check if payload URI is consistent with http request + if (!decodedURIPathFromNode.equals(path)) { + throw new InvalidURIException(decodedURIPathFromNode, path); + } + + // validate format of input node + this.validateInputNode(node); + return createNodeService.createNode(node, path, principal); } + private void validateInputNode(Node node) { + + if (node instanceof LinkNode) { + LinkNode linkNode = (LinkNode) node; + String target = linkNode.getTarget(); + // I validate it here to add context easily + if (target == null) { + throw new InvalidArgumentException("LinkNode in payload has no target element specified"); + } + + URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority); + } + + } + } diff --git a/src/main/java/it/inaf/oats/vospace/CreateNodeService.java b/src/main/java/it/inaf/oats/vospace/CreateNodeService.java index 516f110643e7fdcb2e108527a740a938d6e61b72..83e4dfcac8a942f554bc08a2a73750c3723b274a 100644 --- a/src/main/java/it/inaf/oats/vospace/CreateNodeService.java +++ b/src/main/java/it/inaf/oats/vospace/CreateNodeService.java @@ -10,7 +10,6 @@ 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.InvalidURIException; import it.inaf.oats.vospace.exception.LinkFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.NodeDAO; @@ -19,7 +18,6 @@ import net.ivoa.xml.vospace.v2.Property; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; @@ -30,25 +28,12 @@ public class CreateNodeService { @Autowired private NodeDAO nodeDao; - @Value("${vospace-authority}") - private String authority; - private static final Logger LOG = LoggerFactory.getLogger(CreateNodeService.class); public Node createNode(Node node, String path, User principal) { LOG.debug("createNodeService called for node with URI {} and PATH {}", node.getUri(), path); - // Get Node path (and validates it too) - String decodedURIPathFromNode = URIUtils.returnVosPathFromNodeURI(node.getUri(), authority); - - LOG.debug("createNodeService URI: {} decoded as {}", node.getUri(), decodedURIPathFromNode); - - // Check if payload URI is consistent with http request - if (!decodedURIPathFromNode.equals(path)) { - throw new InvalidURIException(decodedURIPathFromNode, path); - } - // Check if another node is already present at specified path // This checks if the user is trying to insert the root node at "/" too if (nodeDao.listNode(path).isPresent()) { diff --git a/src/main/java/it/inaf/oats/vospace/URIUtils.java b/src/main/java/it/inaf/oats/vospace/URIUtils.java index dbfedf21627bf115b76befd9ca8717e079bff738..0250050fd271a772725b8a1097f4a63a367c1f94 100644 --- a/src/main/java/it/inaf/oats/vospace/URIUtils.java +++ b/src/main/java/it/inaf/oats/vospace/URIUtils.java @@ -51,6 +51,10 @@ public class URIUtils { String resultPath = null; try { + + if(nodeURI == null) + throw new IllegalArgumentException("URI string is null"); + URI uri = new URI(nodeURI); // Check scheme @@ -88,7 +92,7 @@ public class URIUtils { } catch (URISyntaxException e) { throw new InvalidURIException(nodeURI); - } + } return resultPath; diff --git a/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java b/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java index dc3d24526db6033f35130e1baa1845a860345f93..f9dcff79ee7bb9fbbc6ae1928e4ea1ad95796a11 100644 --- a/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java +++ b/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java @@ -30,6 +30,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.LinkNode; import java.util.List; +import java.util.Objects; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -44,17 +45,17 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock @TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true") @AutoConfigureMockMvc public class CreateNodeControllerTest { - + @MockBean private NodeDAO nodeDao; - + @SpyBean @Autowired private CreateNodeController controller; - + @Autowired private MockMvc mockMvc; - + private ContainerNode getContainerParentNode(String path) { ContainerNode parentNode = new ContainerNode(); // Set parent node address at / @@ -66,7 +67,7 @@ public class CreateNodeControllerTest { parentNode.setProperties(List.of(groups)); return parentNode; } - + private ContainerNode getContainerParentNodeWithCreator(String path) { ContainerNode parentNode = new ContainerNode(); // Set parent node address at / @@ -77,7 +78,7 @@ public class CreateNodeControllerTest { parentNode.setProperties(List.of(creator)); return parentNode; } - + private LinkNode getLinkParentNode(String path) { LinkNode parentNode = new LinkNode(); // Set parent node address at / @@ -89,15 +90,15 @@ public class CreateNodeControllerTest { parentNode.setProperties(List.of(groups)); return parentNode; } - + @Test public void testFromJsonToXml() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.json"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNode("/"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -105,17 +106,17 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().isOk()); - + verifyArguments(); } - + @Test public void testFromXmlToJson() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNode("/"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -123,18 +124,18 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); - + verifyArguments(); verify(nodeDao, times(1)).createNode(any()); } - + @Test public void testFromXmlToXml() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNode("/"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -142,18 +143,18 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().isOk()); - + verifyArguments(); verify(nodeDao, times(1)).createNode(any()); } - + @Test public void testFromJsonToJson() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.json"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNode("/"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -161,18 +162,18 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); - + verifyArguments(); verify(nodeDao, times(1)).createNode(any()); } - + @Test public void testURIConsistence() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNode("/"))); - + mockMvc.perform(put("/nodes/mydata2") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -180,20 +181,77 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().is4xxClientError()); - + verifyArguments(); - + + } + + @Test + public void testCreateInternalLinkNode() throws Exception { + String requestBody = getResourceFileContent("create-internal-link-node.xml"); + + when(nodeDao.listNode(eq("/"))) + .thenReturn(Optional.of(getContainerParentNode("/"))); + + mockMvc.perform(put("/nodes/myInternalLink") + .header("Authorization", "Bearer user2_token") + .content(requestBody) + .contentType(MediaType.APPLICATION_XML) + .accept(MediaType.APPLICATION_XML)) + .andDo(print()) + .andExpect(status().isOk()); + + verifyLinkArguments("vos://example.com!vospace/myDummyDataNode1"); + + } + + @Test + public void testCreateExternalLinkNode() throws Exception { + String requestBody = getResourceFileContent("create-external-link-node.xml"); + + when(nodeDao.listNode(eq("/"))) + .thenReturn(Optional.of(getContainerParentNode("/"))); + + mockMvc.perform(put("/nodes/myExternalLink") + .header("Authorization", "Bearer user2_token") + .content(requestBody) + .contentType(MediaType.APPLICATION_XML) + .accept(MediaType.APPLICATION_XML)) + .andDo(print()) + .andExpect(status().is4xxClientError()); + + verifyLinkArguments("vos://external.com!vospace/myDummyDataNode1"); + + } + + @Test + public void testCreateLinkNodeNoTarget() throws Exception { + String requestBody = getResourceFileContent("create-link-node-notarget.xml"); + + when(nodeDao.listNode(eq("/"))) + .thenReturn(Optional.of(getContainerParentNode("/"))); + + mockMvc.perform(put("/nodes/myNoTargetLink") + .header("Authorization", "Bearer user2_token") + .content(requestBody) + .contentType(MediaType.APPLICATION_XML) + .accept(MediaType.APPLICATION_XML)) + .andDo(print()) + .andExpect(status().is4xxClientError()); + + verifyLinkArguments(null); + } - + @Test public void testNodeAlreadyExisting() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNode("/"))); when(nodeDao.listNode(eq("/mydata1"))) .thenReturn(Optional.of(getContainerParentNode("/mydata1"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -201,17 +259,17 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().is4xxClientError()); - + verifyArguments(); } - + @Test public void testContainerNotFound() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.ofNullable(null)); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -219,17 +277,17 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().is4xxClientError()); - + verifyArguments(); } - + @Test public void testLinkNodeFound() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getLinkParentNode("/"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -237,17 +295,17 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().is4xxClientError()); - + verifyArguments(); } - + @Test public void testPermissionDenied() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNode("/"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user1_token") .content(requestBody) @@ -255,17 +313,17 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().is4xxClientError()); - + verifyArguments(); } - + @Test public void testWriteWithOnlyOwnership() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNodeWithCreator("/"))); - + mockMvc.perform(put("/nodes/mydata1") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -273,16 +331,16 @@ public class CreateNodeControllerTest { .accept(MediaType.APPLICATION_XML)) .andDo(print()) .andExpect(status().is2xxSuccessful()); - + verifyArguments(); verify(nodeDao, times(1)).createNode(any()); } - + @Test public void testWriteOwnerAbsent() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNodeWithCreator("/"))); @@ -300,18 +358,18 @@ public class CreateNodeControllerTest { UnstructuredDataNode udn = (UnstructuredDataNode) node; String creator = NodeProperties.getNodePropertyByURI( udn, NodeProperties.CREATOR_URI); - return (creator != null && creator.equals("user2")); + return (creator != null && creator.equals("user2")); })); - + } - + @Test public void testGroupPropertiesAbsent() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node.xml"); - + ContainerNode cdn = getContainerParentNode("/"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(cdn)); @@ -331,16 +389,16 @@ public class CreateNodeControllerTest { udn, NodeProperties.GROUP_READ_URI); String groupWrite = NodeProperties.getNodePropertyByURI( udn, NodeProperties.GROUP_WRITE_URI); - return (groupRead == null && groupWrite.equals("group1 group2")); + return (groupRead == null && groupWrite.equals("group1 group2")); })); - + } - + @Test public void testWriteOwnerMismatch() throws Exception { String requestBody = getResourceFileContent("create-unstructured-data-node-user1.xml"); - + when(nodeDao.listNode(eq("/"))) .thenReturn(Optional.of(getContainerParentNodeWithCreator("/"))); @@ -354,15 +412,15 @@ public class CreateNodeControllerTest { .andExpect(status().is4xxClientError()); // assert createNode is not called - verify(nodeDao, times(0)).createNode(any()); + verify(nodeDao, times(0)).createNode(any()); } - + @Test public void testSubPath() throws Exception { - + String requestBody = getResourceFileContent("create-unstructured-data-node.xml") .replace("/mydata1", "/mydata1/anothernode"); - + mockMvc.perform(put("/nodes/mydata1/anothernode") .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -373,19 +431,19 @@ public class CreateNodeControllerTest { // Using ArgumentCaptor for verifying multiple method invocations ArgumentCaptor<String> argCaptor = ArgumentCaptor.forClass(String.class); - + verify(nodeDao, times(2)).listNode(argCaptor.capture()); - + assertEquals("/mydata1/anothernode", argCaptor.getAllValues().get(0)); assertEquals("/mydata1", argCaptor.getAllValues().get(1)); } - + @Test public void testIllegalChars() throws Exception { - + String requestBody = getResourceFileContent("create-unstructured-data-node.xml") .replace("/mydata1", "/mydata1/?anothernode"); - + String message = mockMvc.perform(put(new URI("/nodes/mydata1/%3Fanothernode")) .header("Authorization", "Bearer user2_token") .content(requestBody) @@ -394,10 +452,10 @@ public class CreateNodeControllerTest { .andDo(print()) .andExpect(status().isBadRequest()) .andReturn().getResolvedException().getMessage(); - + assertTrue(message.contains("contains an illegal character")); } - + private void verifyArguments() { verify(controller).createNode( argThat(node -> { @@ -408,7 +466,19 @@ public class CreateNodeControllerTest { && "ivo://ivoa.net/vospace/core#description".equals(property.getUri()); }), any()); } - + + private void verifyLinkArguments(String target) { + verify(controller).createNode( + argThat(node -> { + LinkNode linkNode = (LinkNode) node; + Property property = linkNode.getProperties().get(0); + return "vos:LinkNode".equals(linkNode.getType()) + && "test value".equals(property.getValue()) + && "ivo://ivoa.net/vospace/core#description".equals(property.getUri()) + && Objects.equals(target, linkNode.getTarget()); + }), any()); + } + protected static String getResourceFileContent(String fileName) throws Exception { try (InputStream in = CreateNodeControllerTest.class.getClassLoader().getResourceAsStream(fileName)) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); 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 c85b48a9aa93c7fb9a2503ad9257c93c4b3a5af5..1b07a7b29c265e14af1273e4e2cc3d923937b086 100644 --- a/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java +++ b/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java @@ -17,6 +17,7 @@ import java.util.Set; import javax.sql.DataSource; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; +import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.View; import static org.junit.jupiter.api.Assertions.assertEquals; import net.ivoa.xml.vospace.v2.Node; @@ -65,6 +66,23 @@ public class NodeDAOTest { assertEquals(retrievedNode.getAccepts().get(0).getUri(), dataNode.getAccepts().get(0).getUri()); assertEquals(retrievedNode.getProvides().get(0).getUri(), dataNode.getProvides().get(0).getUri()); } + + @Test + public void testCreateLinkNode() { + LinkNode linkNode = new LinkNode(); + + String target = "vos://example.com!vospace/myData1"; + + linkNode.setUri("vos://example.com!vospace/myLink1"); + linkNode.setTarget(target); + + dao.createNode(linkNode); + + Node retrievedNode = dao.listNode("/myLink1").get(); + + assertTrue(retrievedNode instanceof LinkNode); + assertEquals(target, ((LinkNode) retrievedNode).getTarget()); + } @Test public void testListNode() { diff --git a/src/test/resources/create-external-link-node.xml b/src/test/resources/create-external-link-node.xml new file mode 100644 index 0000000000000000000000000000000000000000..b723379943a1a995c88df2e4ca3046ed628a6a59 --- /dev/null +++ b/src/test/resources/create-external-link-node.xml @@ -0,0 +1,11 @@ +<vos:node xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" xsi:type="vos:LinkNode" uri="vos://example.com!vospace/myExternalLink"> + <vos:properties> + <vos:property uri="ivo://ivoa.net/vospace/core#description">test value</vos:property> + </vos:properties> + <vos:target>vos://external.com!vospace/myDummyDataNode1</vos:target> + <vos:accepts/> + <vos:provides/> + <vos:capabilities/> +</vos:node> \ No newline at end of file diff --git a/src/test/resources/create-internal-link-node.xml b/src/test/resources/create-internal-link-node.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d60b5f1e43b812d7ae1dc78ad91428f55a55859 --- /dev/null +++ b/src/test/resources/create-internal-link-node.xml @@ -0,0 +1,11 @@ +<vos:node xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" xsi:type="vos:LinkNode" uri="vos://example.com!vospace/myInternalLink"> + <vos:properties> + <vos:property uri="ivo://ivoa.net/vospace/core#description">test value</vos:property> + </vos:properties> + <vos:target>vos://example.com!vospace/myDummyDataNode1</vos:target> + <vos:accepts/> + <vos:provides/> + <vos:capabilities/> +</vos:node> \ No newline at end of file diff --git a/src/test/resources/create-link-node-notarget.xml b/src/test/resources/create-link-node-notarget.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ab5488311ab674cb72d678e9269cd649d0b37e6 --- /dev/null +++ b/src/test/resources/create-link-node-notarget.xml @@ -0,0 +1,10 @@ +<vos:node xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" xsi:type="vos:LinkNode" uri="vos://example.com!vospace/myNoTargetLink"> + <vos:properties> + <vos:property uri="ivo://ivoa.net/vospace/core#description">test value</vos:property> + </vos:properties> + <vos:accepts/> + <vos:provides/> + <vos:capabilities/> +</vos:node> \ No newline at end of file