/* * 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.ServletRapClient; 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.datamodel.Views; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.exception.ProtocolNotSupportedException; import it.inaf.oats.vospace.persistence.LinkedServiceDAO; import it.inaf.oats.vospace.persistence.LocationDAO; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.model.Location; import it.inaf.oats.vospace.persistence.model.LocationType; 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.DataNode; import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Param; import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.View; 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 static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.springframework.beans.factory.annotation.Autowired; 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.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.TestPropertySource; @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = {"vospace-authority=example.com!vospace", "file-service-url=http://file-service"}) public class UriServiceTest { @MockBean private NodeDAO nodeDAO; @MockBean private LocationDAO locationDAO; @MockBean private ServletRapClient rapClient; @MockBean private CreateNodeService createNodeService; @MockBean private LinkedServiceDAO linkedServiceDAO; @MockBean private FileServiceClient fileServiceClient; @Autowired private HttpServletRequest servletRequest; @Autowired private UriService uriService; @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; } } public final static String PORTAL_URL = "http://portalurl.ia2.inaf.it/portal/files"; @BeforeEach public void init() { Location location = new Location(); location.setType(LocationType.ASYNC); when(locationDAO.getNodeLocation(any())).thenReturn(Optional.of(location)); when(linkedServiceDAO.isLinkedServiceUrl(eq(PORTAL_URL))).thenReturn(Boolean.TRUE); } @Test public void testPublicUrl() { String dataUri = "vos://example.com!vospace/mydata1"; Node node = new DataNode(); node.setUri(dataUri); Property property = new Property(); property.setUri(NodeProperties.PUBLIC_READ_URI); property.setValue("true"); node.getProperties().add(property); when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); JobSummary job = getJob(); Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, uriService.getTransfer(job)); assertEquals("http://file-service/mydata1?jobId=job-id", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } @Test public void testPrivateUrl() { String dataUri = "vos://example.com!vospace/mydata1"; Node node = new DataNode(); node.setUri(dataUri); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); node.getProperties().add(creator); Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); node.getProperties().add(readgroup); when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); User user = mock(User.class); when(user.getAccessToken()).thenReturn(""); when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); when(rapClient.exchangeToken(argThat(req -> { assertEquals("", req.getSubjectToken()); assertEquals("http://file-service/mydata1", req.getResource()); return true; }), any())).thenReturn(""); JobSummary job = getJob(); Transfer tr = uriService.getTransfer(job); Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, tr); assertEquals("http://file-service/mydata1?jobId=job-id&token=", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } @Test public void testPrivateUrlPermissionDenied() { String dataUri = "vos://example.com!vospace/mydata1"; Node node = new DataNode(); node.setUri(dataUri); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user3"); node.getProperties().add(creator); Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1000"); node.getProperties().add(readgroup); when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); User user = mock(User.class); when(user.getAccessToken()).thenReturn(""); when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); when(rapClient.exchangeToken(argThat(req -> { assertEquals("", req.getSubjectToken()); assertEquals("http://file-service/mydata1", req.getResource()); return true; }), any())).thenReturn(""); JobSummary job = getJob(); Transfer tr = uriService.getTransfer(job); assertThrows(PermissionDeniedException.class, () -> { uriService.getNegotiatedTransfer(job, tr); }); } @Test public void testPrivateUrlNodeBusy() { String dataUri = "vos://example.com!vospace/mydata1"; DataNode node = new DataNode(); node.setUri(dataUri); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); node.getProperties().add(creator); Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); node.getProperties().add(readgroup); node.setBusy(Boolean.TRUE); when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); User user = mock(User.class); when(user.getAccessToken()).thenReturn(""); when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); when(rapClient.exchangeToken(argThat(req -> { assertEquals("", req.getSubjectToken()); assertEquals("http://file-service/mydata1", req.getResource()); return true; }), any())).thenReturn(""); JobSummary job = getJob(); Transfer tr = uriService.getTransfer(job); assertThrows(NodeBusyException.class, () -> { uriService.getNegotiatedTransfer(job, tr); }); } @Test public void pushToNonexistentNode() { ContainerNode node = new ContainerNode(); node.setUri("vos://example.com!vospace/mydata1"); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); node.getProperties().add(creator); Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); node.getProperties().add(readgroup); DataNode dnode = new DataNode(); dnode.setUri("vos://example.com!vospace/mydata1/mydata2"); dnode.getProperties().add(creator); dnode.getProperties().add(readgroup); Property writegroup = new Property(); writegroup.setUri(NodeProperties.GROUP_WRITE_URI); writegroup.setValue("group1"); dnode.getProperties().add(writegroup); when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); when(nodeDAO.listNode(eq("/mydata1/mydata2"))).thenReturn(Optional.empty()); User user = mock(User.class); when(user.getAccessToken()).thenReturn(""); when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); when(rapClient.exchangeToken(argThat(req -> { assertEquals("", req.getSubjectToken()); assertEquals("http://file-service/mydata1/mydata2", req.getResource()); return true; }), any())).thenReturn(""); JobSummary job = getPushToVoSpaceJob(); Transfer tr = uriService.getTransfer(job); when(createNodeService.createNode(any(), any(), eq(user))).thenReturn(dnode); Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, tr); verify(createNodeService, times(1)).createNode(any(), any(), eq(user)); assertEquals("http://file-service/mydata1/mydata2?jobId=job-id2&token=", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } @Test public void pullFromLinkNode() { // URI of pull target node String targetOfPull = "vos://example.com!vospace/mylink1"; String targetOfLink = "vos://example.com!vospace/mydummydata1"; // Define node properties Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); // Define link node as target LinkNode lnode = new LinkNode(); lnode.setUri(targetOfPull); lnode.getProperties().add(creator); lnode.getProperties().add(readgroup); lnode.setTarget(targetOfLink); DataNode dnode = new DataNode(); dnode.setUri(targetOfLink); dnode.getProperties().add(creator); dnode.getProperties().add(readgroup); when(nodeDAO.listNode(eq(NodeUtils.getVosPath(lnode)))).thenReturn(Optional.of(lnode)); when(nodeDAO.listNode(eq(NodeUtils.getVosPath(dnode)))).thenReturn(Optional.of(dnode)); User user = mock(User.class); when(user.getAccessToken()).thenReturn(""); when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); when(rapClient.exchangeToken(argThat(req -> { assertEquals("", req.getSubjectToken()); assertEquals("http://file-service/mydummydata1", req.getResource()); return true; }), any())).thenReturn(""); JobSummary job = getPullFromVoSpaceJob(targetOfPull); Transfer tr = uriService.getTransfer(job); Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, tr); assertEquals("http://file-service" + NodeUtils.getVosPath(dnode) + "?jobId=job-id-pull&token=", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } @Test public void pullFromCircularLinkNode() { // URI of pull target node String targetOfPull = "vos://example.com!vospace/mylink1"; String targetOfLink = "vos://example.com!vospace/mylink2"; // Define node properties Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); // Define link node as target LinkNode lnode = new LinkNode(); lnode.setUri(targetOfPull); lnode.getProperties().add(creator); lnode.getProperties().add(readgroup); lnode.setTarget(targetOfLink); LinkNode dnode = new LinkNode(); dnode.setUri(targetOfLink); dnode.getProperties().add(creator); dnode.getProperties().add(readgroup); // Circular reference dnode.setTarget(targetOfPull); when(nodeDAO.listNode(eq(NodeUtils.getVosPath(lnode)))).thenReturn(Optional.of(lnode)); when(nodeDAO.listNode(eq(NodeUtils.getVosPath(dnode)))).thenReturn(Optional.of(dnode)); User user = mock(User.class); when(user.getAccessToken()).thenReturn(""); when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); JobSummary job = getPullFromVoSpaceJob(targetOfPull); Transfer tr = uriService.getTransfer(job); assertThrows(InternalFaultException.class, () -> { uriService.getNegotiatedTransfer(job, tr); }); } @Test public void setNodeRemoteLocationTest() { String nodeUri = "vos://example.com!vospace/test/f1/lbtfile.fits"; String contentUri = "http://archive.lbto.org/files/lbtfile.fits"; Location location = new Location(); location.setId(5); when(locationDAO.findPortalLocation(eq("archive.lbto.org"))).thenReturn(Optional.of(location)); uriService.setNodeRemoteLocation(nodeUri, contentUri); verify(nodeDAO).setNodeLocation(eq("/test/f1/lbtfile.fits"), eq(5), eq("lbtfile.fits")); } @Test public void testSetSyncTransferEndpointsPullFromVoSpace() { mockPublicNode(); Transfer transfer = new Transfer(); transfer.setTarget("vos://example.com!vospace/mydata1"); transfer.setDirection("pullFromVoSpace"); Protocol protocol1 = new Protocol(); protocol1.setUri("ivo://ivoa.net/vospace/core#httpget"); transfer.getProtocols().add(protocol1); Protocol protocol2 = new Protocol(); protocol2.setUri("invalid_protocol"); transfer.getProtocols().add(protocol2); JobSummary job = new JobSummary(); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); assertEquals(2, transfer.getProtocols().size()); Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, transfer); // invalid protocol is removed assertEquals(1, negotiatedTransfer.getProtocols().size()); assertEquals("ivo://ivoa.net/vospace/core#httpget", negotiatedTransfer.getProtocols().get(0).getUri()); } @Test public void testSetSyncTransferEndpointsPushToVoSpace() { Node node = mockPublicNode(); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); node.getProperties().add(creator); User user = mock(User.class); when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); Transfer transfer = new Transfer(); transfer.setTarget("vos://example.com!vospace/mydata1"); transfer.setDirection("pushToVoSpace"); Protocol protocol1 = new Protocol(); protocol1.setUri("ivo://ivoa.net/vospace/core#httpput"); transfer.getProtocols().add(protocol1); Protocol protocol2 = new Protocol(); protocol2.setUri("invalid_protocol"); transfer.getProtocols().add(protocol2); JobSummary job = new JobSummary(); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); assertEquals(2, transfer.getProtocols().size()); Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, transfer); // invalid protocol is removed assertEquals(1, negotiatedTransfer.getProtocols().size()); assertEquals("ivo://ivoa.net/vospace/core#httpput", negotiatedTransfer.getProtocols().get(0).getUri()); } @Test public void testSetSyncTransferEndpointsUnsupportedProtocol() { Transfer transfer = new Transfer(); transfer.setTarget("vos://example.com!vospace/mydata1"); transfer.setDirection("pullFromVoSpace"); Protocol protocol = new Protocol(); protocol.setUri("invalid_protocol"); transfer.getProtocols().add(protocol); JobSummary job = new JobSummary(); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); try { uriService.getNegotiatedTransfer(job, transfer); fail("Expected ProtocolNotSupportedException"); } catch (ProtocolNotSupportedException ex) { } } @Test public void testSetSyncTransferEndpointsNoProtocols() { Transfer transfer = new Transfer(); transfer.setTarget("vos://example.com!vospace/mydata1"); transfer.setDirection("pullFromVoSpace"); JobSummary job = new JobSummary(); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); try { uriService.getNegotiatedTransfer(job, transfer); fail("Expected InvalidArgumentException"); } catch (InvalidArgumentException ex) { } } @Test public void testTarArchiveViewEndpoint() { testArchiveViewEndpoint(Views.TAR_VIEW_URI); } @Test public void testZipArchiveViewEndpoint() { testArchiveViewEndpoint(Views.ZIP_VIEW_URI); } @Test public void testInvalidTransferNoProtocols() { Transfer transfer = new Transfer(); transfer.setDirection("pullFromVoSpace"); transfer.setTarget("vos://example.com!vospace/file1"); JobSummary job = new JobSummary(); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); mockPublicNode("file1"); mockPublicNode("file2"); InvalidArgumentException ex = assertThrows(InvalidArgumentException.class, () -> { uriService.getNegotiatedTransfer(job, transfer); }); assertTrue(ex.getMessage().contains("no protocol")); } private void testArchiveViewEndpoint(String viewUri) { Transfer transfer = new Transfer(); transfer.setDirection("pullFromVoSpace"); transfer.setTarget("vos://example.com!vospace/parent_dir"); Protocol protocol = new Protocol(); protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); transfer.getProtocols().add(protocol); View view = new View(); view.setUri(viewUri); Param param1 = new Param(); param1.setUri(viewUri + "/include"); param1.setValue("file1"); view.getParam().add(param1); Param param2 = new Param(); param2.setUri(viewUri + "/include"); param2.setValue("file2"); view.getParam().add(param2); transfer.setView(view); JobSummary job = new JobSummary(); job.setJobId("archive-job-id"); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); mockPublicNode("parent_dir"); mockPublicNode("parent_dir/file1"); mockPublicNode("parent_dir/file2"); when(fileServiceClient.startArchiveJob(any(), any())) .thenReturn("http://file-service/mockarchive"); uriService.getNegotiatedTransfer(job, transfer); verify(fileServiceClient, times(1)).startArchiveJob(transfer, "archive-job-id"); } private Node mockPublicNode() { return mockPublicNode("mydata1"); } private Node mockPublicNode(String name) { DataNode node = new DataNode(); node.setUri("vos://example.com!vospace/" + name); Property publicProperty = new Property(); publicProperty.setUri(NodeProperties.PUBLIC_READ_URI); publicProperty.setValue(String.valueOf(true)); node.getProperties().add(publicProperty); when(nodeDAO.listNode(eq("/" + name))).thenReturn(Optional.of(node)); return node; } private JobSummary getJob() { Transfer transfer = new Transfer(); transfer.setTarget("vos://example.com!vospace/mydata1"); transfer.setDirection(JobService.JobDirection.pullFromVoSpace.toString()); Protocol protocol = new Protocol(); protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); transfer.getProtocols().add(protocol); JobSummary job = new JobSummary(); job.setJobId("job-id"); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); return job; } private JobSummary getPushToVoSpaceJob() { Transfer transfer = new Transfer(); transfer.setTarget("vos://example.com!vospace/mydata1/mydata2"); transfer.setDirection(JobService.JobDirection.pushToVoSpace.toString()); Protocol protocol = new Protocol(); protocol.setUri("ivo://ivoa.net/vospace/core#httpput"); transfer.getProtocols().add(protocol); JobSummary job = new JobSummary(); job.setJobId("job-id2"); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); return job; } private JobSummary getPullFromVoSpaceJob(String target) { Transfer transfer = new Transfer(); transfer.setTarget(target); transfer.setDirection(JobService.JobDirection.pullFromVoSpace.toString()); Protocol protocol = new Protocol(); protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); transfer.getProtocols().add(protocol); JobSummary job = new JobSummary(); job.setJobId("job-id-pull"); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); return job; } }