diff --git a/docs/Create-And-Publish-Virtual-Study.md b/docs/Create-And-Publish-Virtual-Study.md new file mode 100644 index 00000000000..14ebbf9c438 --- /dev/null +++ b/docs/Create-And-Publish-Virtual-Study.md @@ -0,0 +1,61 @@ +# Create and Publish Virtual Study + +A [Virtual Study](./user-guide/faq.md#what-is-a-virtual-study) defines a subset or a combination of samples from one or more studies in the system. + +*Note*: To publish or un-publish a virtual study, your cBioPortal instance must be configured with `session.endpoint.publisher-api-key` in the `application.properties`. + +## Create Virtual Study + +To create a virtual study in cBioPortal, follow these steps: + +1. Define the desired filters on the study or multiple studies summary page. +2. Click the button with the bookmark icon () in the top right corner of the screen. +3. Provide a title and description, then click the Save button. You will see a link that looks like: + +``` +https:///study?id= +``` + +4. Save the virtual study link or ID if you want to publish it. + +If you are logged in, this virtual study will appear in the `My Virtual Studies` section on the landing page. +You can always find the ID of the virtual study from the URL of the page that opens after clicking on it. + +## Publish Virtual Study + +To publish a virtual study, you need to supply the publisher API key in the `X-PUBLISHER-API-KEY` header. + +Here is a curl command to publish a virtual study: +```shell +curl \ + -X POST \ + -H 'X-PUBLISHER-API-KEY: ' \ + -v 'http:///api/public_virtual_studies/' +``` +The published virtual study will appear under the `Public Virtual Studies` section (next to the `My Virtual Studies` section) on the landing page for all users of cBioPortal. + +While publishing, you can specify the PubMed ID (`pmid`) and `typeOfCancerId` of the virtual study using the following command: +```shell +curl \ + -X POST \ + -H 'X-PUBLISHER-API-KEY: ' \ + -v 'http:///api/public_virtual_studies/?pmid=&typeOfCancerId=' +``` + +The type of cancer code should match the known types of cancers in the cBioPortal database. +If the type of cancer is specified, the published virtual study will appear under the respective cancer section on the landing page. +Specifying the `pmid` enables a link to the PubMed page of the study. + +## Un-publish Virtual Study + +To un-publish a virtual study, you need to supply the publisher API key in the `X-PUBLISHER-API-KEY` header. +After un-publishing, virtual study will no longer be displayed in the `Public Virtual Studies` section on the landing page. +However, it reappears in the `My Virtual Studies` section for the owner. + +Here is the command to un-publish a virtual study: +```shell +curl \ + -X DELETE \ + -H 'X-PUBLISHER-API-KEY: ' \ + -v 'http:///api/public_virtual_studies/' +``` \ No newline at end of file diff --git a/docs/Data-Loading.md b/docs/Data-Loading.md index 39f27d9b385..9465f051edb 100644 --- a/docs/Data-Loading.md +++ b/docs/Data-Loading.md @@ -58,3 +58,6 @@ To remove a study, the [cbioportalImporter script](/Data-Loading-Maintaining-Stu ## Example studies Examples for the different types of data are available on the [File Formats](/File-Formats.md) page. The Provisional TCGA studies, downloadable from the [Data Sets section](https://www.cbioportal.org/datasets) are complete studies that can be used as reference when creating data files. + +## Public Virtual Studies +If your new study data is a subset or a combination of existing studies in the system, consider using [Public Virtual Studies](./Create-And-Publish-Virtual-Study.md) instead of duplicating data. \ No newline at end of file diff --git a/pom.xml b/pom.xml index b3866426742..20936fa44d7 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,7 @@ 3.14.0 4.17.0 7.1.0 + 5.2.1 @@ -346,7 +347,14 @@ sentry-spring-boot-starter-jakarta ${sentry.version} - + + org.apache.httpcomponents.client5 + httpclient5 + ${apache_httpclient.version} + test + + + diff --git a/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java b/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java index f1171fd48db..c6bd8d04645 100644 --- a/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java +++ b/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java @@ -2,12 +2,10 @@ import org.cbioportal.persistence.cachemaputil.CacheMapUtil; import org.cbioportal.security.CancerStudyPermissionEvaluator; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -17,28 +15,22 @@ // We are allowing users to enable method_authorization if optional_oauth2 is selected @ConditionalOnExpression("{'oauth2','saml', 'saml_plus_basic'}.contains('${authenticate}') or ('optional_oauth2' eq '${authenticate}' and 'true' eq '${security.method_authorization_enabled}')") public class MethodSecurityConfig { - @Value("${app.name:}") - private String appName; - - @Value("${filter_groups_by_appname:true}") - private String doFilterGroupsByAppName; - - @Value("${always_show_study_group:}") - private String alwaysShowCancerStudyGroup; - - @Autowired - private CacheMapUtil cacheMapUtil; @Bean - public MethodSecurityExpressionHandler createExpressionHandler() { + public CancerStudyPermissionEvaluator cancerStudyPermissionEvaluator( + @Value("${app.name:}") String appName, + @Value("${filter_groups_by_appname:true}") String doFilterGroupsByAppName, + @Value("${always_show_study_group:}") String alwaysShowCancerStudyGroup, + CacheMapUtil cacheMapUtil + ) { + return new CancerStudyPermissionEvaluator(appName, doFilterGroupsByAppName, alwaysShowCancerStudyGroup, cacheMapUtil); + } + + @Bean + public MethodSecurityExpressionHandler createExpressionHandler(CancerStudyPermissionEvaluator cancerStudyPermissionEvaluator) { DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - expressionHandler.setPermissionEvaluator(cancerStudyPermissionEvaluator()); + expressionHandler.setPermissionEvaluator(cancerStudyPermissionEvaluator); return expressionHandler; } - - @Bean - public CancerStudyPermissionEvaluator cancerStudyPermissionEvaluator() { - return new CancerStudyPermissionEvaluator(appName, doFilterGroupsByAppName, alwaysShowCancerStudyGroup, cacheMapUtil); - } } \ No newline at end of file diff --git a/src/main/java/org/cbioportal/service/exception/AccessForbiddenException.java b/src/main/java/org/cbioportal/service/exception/AccessForbiddenException.java new file mode 100644 index 00000000000..d9dd8b64bd5 --- /dev/null +++ b/src/main/java/org/cbioportal/service/exception/AccessForbiddenException.java @@ -0,0 +1,7 @@ +package org.cbioportal.service.exception; + +public class AccessForbiddenException extends RuntimeException { + public AccessForbiddenException(String message) { + super(message); + } +} diff --git a/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java b/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java index c408f2f2139..af9749ee052 100644 --- a/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java +++ b/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java @@ -4,13 +4,22 @@ import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import com.mongodb.BasicDBObject; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; +import org.cbioportal.web.parameter.VirtualStudy; +import org.cbioportal.web.parameter.VirtualStudyData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -18,6 +27,8 @@ @Component public class SessionServiceRequestHandler { + private static final Logger LOG = LoggerFactory.getLogger(SessionServiceRequestHandler.class); + @Value("${session.service.url:}") private String sessionServiceURL; @@ -62,4 +73,81 @@ public String getSessionDataJson(SessionType type, String id) throws Exception { return responseEntity.getBody(); } + /** + * Gets virtual study by id + * @param id - id of the virtual study to read + * @return virtual study + */ + public VirtualStudy getVirtualStudyById(String id) { + ResponseEntity responseEntity = new RestTemplate() + .exchange(sessionServiceURL + "/virtual_study/" + id, + HttpMethod.GET, + new HttpEntity<>(getHttpHeaders()), + VirtualStudy.class); + HttpStatusCode statusCode = responseEntity.getStatusCode(); + VirtualStudy virtualStudy = responseEntity.getBody(); + if (!statusCode.is2xxSuccessful() || virtualStudy == null) { + LOG.error("The downstream server replied with statusCode={} and body={}." + + " Replying with the same status code to the client.", + statusCode, virtualStudy); + throw new IllegalStateException("The downstream server response is not successful"); + } + return responseEntity.getBody(); + } + + /** + * Get list of virtual studies accessible to user + * @param username - user for whom get list of virtual studies + * @return - list of virtual studies + */ + public List getVirtualStudiesAccessibleToUser(String username) { + BasicDBObject basicDBObject = new BasicDBObject(); + basicDBObject.put("data.users", username); + ResponseEntity> responseEntity = new RestTemplate().exchange( + sessionServiceURL + "/virtual_study/query/fetch", + HttpMethod.POST, + new HttpEntity<>(basicDBObject.toString(), getHttpHeaders()), + new ParameterizedTypeReference<>() { + }); + + return responseEntity.getBody(); + } + + /** + * Creates a virtual study out of virtual study definition (aka virtualStudyData) + * @param virtualStudyData - definition of virtual study + * @return virtual study object with id and the virtualStudyData + */ + public VirtualStudy createVirtualStudy(VirtualStudyData virtualStudyData) { + ResponseEntity responseEntity = new RestTemplate().exchange( + sessionServiceURL + "/virtual_study", + HttpMethod.POST, + new HttpEntity<>(virtualStudyData, getHttpHeaders()), + new ParameterizedTypeReference<>() { + }); + + return responseEntity.getBody(); + } + + + /** + * Soft delete of the virtual study by de-associating all assigned users. + * @param id - id of virtual study to soft delete + */ + public void softRemoveVirtualStudy(String id) { + VirtualStudy virtualStudy = getVirtualStudyById(id); + VirtualStudyData data = virtualStudy.getData(); + data.setUsers(Collections.emptySet()); + updateVirtualStudy(virtualStudy); + } + + /** + * Updates virtual study + * @param virtualStudy - virtual study to update + */ + public void updateVirtualStudy(VirtualStudy virtualStudy) { + new RestTemplate() + .put(sessionServiceURL + "/virtual_study/" + virtualStudy.getId(), + new HttpEntity<>(virtualStudy.getData(), getHttpHeaders())); + } } diff --git a/src/main/java/org/cbioportal/web/PublicVirtualStudiesController.java b/src/main/java/org/cbioportal/web/PublicVirtualStudiesController.java new file mode 100644 index 00000000000..c48ac446041 --- /dev/null +++ b/src/main/java/org/cbioportal/web/PublicVirtualStudiesController.java @@ -0,0 +1,139 @@ +package org.cbioportal.web; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.cbioportal.service.CancerTypeService; +import org.cbioportal.service.exception.AccessForbiddenException; +import org.cbioportal.service.exception.CancerTypeNotFoundException; +import org.cbioportal.service.util.SessionServiceRequestHandler; +import org.cbioportal.web.parameter.VirtualStudy; +import org.cbioportal.web.parameter.VirtualStudyData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +@Controller +@RequestMapping("/api/public_virtual_studies") +public class PublicVirtualStudiesController { + + private static final Logger LOG = LoggerFactory.getLogger(PublicVirtualStudiesController.class); + + public static final String ALL_USERS = "*"; + + private final String requiredPublisherApiKey; + + private final SessionServiceRequestHandler sessionServiceRequestHandler; + + private final CancerTypeService cancerTypeService; + + public PublicVirtualStudiesController( + @Value("${session.endpoint.publisher-api-key:}") String requiredPublisherApiKey, + SessionServiceRequestHandler sessionServiceRequestHandler, + CancerTypeService cancerTypeService + ) { + this.requiredPublisherApiKey = requiredPublisherApiKey; + this.sessionServiceRequestHandler = sessionServiceRequestHandler; + this.cancerTypeService = cancerTypeService; + } + + @GetMapping + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = VirtualStudy.class))) + public ResponseEntity> getPublicVirtualStudies() { + List virtualStudies = sessionServiceRequestHandler.getVirtualStudiesAccessibleToUser(ALL_USERS); + return new ResponseEntity<>(virtualStudies, HttpStatus.OK); + } + + @PostMapping("/{id}") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = VirtualStudy.class))) + public ResponseEntity publishVirtualStudy( + @PathVariable String id, + @RequestHeader(value = "X-PUBLISHER-API-KEY") String providedPublisherApiKey, + @RequestParam(required = false) String typeOfCancerId, + @RequestParam(required = false) String pmid + ) { + ensureProvidedPublisherApiKeyCorrect(providedPublisherApiKey); + publishVirtualStudy(id, typeOfCancerId, pmid); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{id}") + @ApiResponse(responseCode = "200", description = "OK") + public ResponseEntity unPublishVirtualStudy( + @PathVariable String id, + @RequestHeader(value = "X-PUBLISHER-API-KEY") String providedPublisherApiKey + ) { + ensureProvidedPublisherApiKeyCorrect(providedPublisherApiKey); + unPublishVirtualStudy(id); + return ResponseEntity.ok().build(); + } + + /** + * Publishes virtual study optionally updating metadata fields + * @param id - id of public virtual study to publish + * @param typeOfCancerId - if specified (not null) update type of cancer of published virtual study + * @param pmid - if specified (not null) update PubMed ID of published virtual study + */ + private void publishVirtualStudy(String id, String typeOfCancerId, String pmid) { + VirtualStudy virtualStudyDataToPublish = sessionServiceRequestHandler.getVirtualStudyById(id); + VirtualStudyData virtualStudyData = virtualStudyDataToPublish.getData(); + updateStudyMetadataFieldsIfSpecified(virtualStudyData, typeOfCancerId, pmid); + virtualStudyData.setUsers(Set.of(ALL_USERS)); + sessionServiceRequestHandler.updateVirtualStudy(virtualStudyDataToPublish); + } + + /** + * Un-publish virtual study + * @param id - id of public virtual study to un-publish + */ + private void unPublishVirtualStudy(String id) { + VirtualStudy virtualStudyToUnPublish = sessionServiceRequestHandler.getVirtualStudyById(id); + if (virtualStudyToUnPublish == null) { + throw new NoSuchElementException("The virtual study with id=" + id + " has not been found in the public list."); + } + VirtualStudyData virtualStudyData = virtualStudyToUnPublish.getData(); + Set users = virtualStudyData.getUsers(); + if (users == null || users.isEmpty() || !users.contains(ALL_USERS)) { + throw new NoSuchElementException("The virtual study with id=" + id + " has not been found in the public list."); + } + virtualStudyData.setUsers(Set.of(virtualStudyData.getOwner())); + sessionServiceRequestHandler.updateVirtualStudy(virtualStudyToUnPublish); + } + + private void ensureProvidedPublisherApiKeyCorrect(String providedPublisherApiKey) { + if (requiredPublisherApiKey.isBlank() + || !requiredPublisherApiKey.equals(providedPublisherApiKey)) { + throw new AccessForbiddenException("The provided publisher API key is not correct."); + } + } + + private void updateStudyMetadataFieldsIfSpecified(VirtualStudyData virtualStudyData, String typeOfCancerId, String pmid) { + if (typeOfCancerId != null) { + try { + cancerTypeService.getCancerType(typeOfCancerId); + virtualStudyData.setTypeOfCancerId(typeOfCancerId); + } catch (CancerTypeNotFoundException e) { + LOG.error("No cancer type with id={} were found.", typeOfCancerId); + throw new IllegalArgumentException( "The cancer type is not valid: " + typeOfCancerId); + } + } + if (pmid != null) { + virtualStudyData.setPmid(pmid); + } + } + +} diff --git a/src/main/java/org/cbioportal/web/SessionServiceController.java b/src/main/java/org/cbioportal/web/SessionServiceController.java index 6a2395cd469..83cf95c932a 100644 --- a/src/main/java/org/cbioportal/web/SessionServiceController.java +++ b/src/main/java/org/cbioportal/web/SessionServiceController.java @@ -58,6 +58,8 @@ import java.util.Set; import java.util.regex.Pattern; +import static org.cbioportal.web.PublicVirtualStudiesController.ALL_USERS; + @Controller @RequestMapping("/api/session") public class SessionServiceController { @@ -132,12 +134,17 @@ private ResponseEntity addSession( if (type.equals(Session.SessionType.virtual_study) || type.equals(Session.SessionType.group)) { // JSON from file to Object VirtualStudyData virtualStudyData = sessionServiceObjectMapper.readValue(body.toString(), VirtualStudyData.class); + //TODO sanitize what's supplied. e.g. anonymous user should not specify the users field! if (isAuthorized()) { - virtualStudyData.setOwner(userName()); + String userName = userName(); + if (userName.equals(ALL_USERS)) { + throw new IllegalStateException("Illegal username " + ALL_USERS + " for assigning virtual studies."); + } + virtualStudyData.setOwner(userName); if ((operation.isPresent() && operation.get().equals(SessionOperation.save)) || type.equals(Session.SessionType.group)) { - virtualStudyData.setUsers(Collections.singleton(userName())); + virtualStudyData.setUsers(Collections.singleton(userName)); } } @@ -246,7 +253,8 @@ public ResponseEntity> getUserStudies() throws JsonProcessing httpEntity, new ParameterizedTypeReference>() {}); - return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK); + List virtualStudyList = responseEntity.getBody(); + return new ResponseEntity<>(virtualStudyList, HttpStatus.OK); } catch (Exception exception) { LOG.error("Error occurred", exception); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); @@ -260,7 +268,7 @@ public ResponseEntity> getUserStudies() throws JsonProcessing content = @Content(schema = @Schema(implementation = Session.class))) public ResponseEntity addSession(@PathVariable Session.SessionType type, @RequestBody JSONObject body) throws IOException { - + //FIXME? anonymous user can create sessions. Do we really want that? https://github.com/cBioPortal/cbioportal/issues/10843 return addSession(type, Optional.empty(), body); } @@ -268,7 +276,7 @@ public ResponseEntity addSession(@PathVariable Session.SessionType type @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = Session.class))) public ResponseEntity addUserSavedVirtualStudy(@RequestBody JSONObject body) throws IOException { - + //FIXME? anonymous user can create virtual studies. Do we really want that? https://github.com/cBioPortal/cbioportal/issues/10843 return addSession(Session.SessionType.virtual_study, Optional.of(SessionOperation.save), body); } diff --git a/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java b/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java index 75bc5c55e6e..1c24d60c431 100644 --- a/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java +++ b/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import java.util.Iterator; +import java.util.NoSuchElementException; // TODO @@ -162,6 +163,12 @@ public ResponseEntity handleDataAccessTokenProhibitedUserExceptio return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED); } + @ExceptionHandler(AccessForbiddenException.class) + public ResponseEntity handleAccessForbiddenException() { + ErrorResponse response = new ErrorResponse("The access is forbidden."); + return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED); + } + @ExceptionHandler(TokenNotFoundException.class) public ResponseEntity handleTokenNotFoundException() { ErrorResponse response = new ErrorResponse("Specified token cannot be found"); @@ -201,4 +208,9 @@ public ResponseEntity handleBadSqlGrammar(BadSqlGrammarException HttpStatus.INTERNAL_SERVER_ERROR ); } + + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) { + return new ResponseEntity<>(new ErrorResponse(ex.getMessage()), HttpStatus.NOT_FOUND); + } } diff --git a/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java b/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java index 98cbfa9bd2d..54a5a88a1d6 100644 --- a/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java +++ b/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java @@ -21,6 +21,9 @@ public class VirtualStudyData implements Serializable { private Long lastUpdated = System.currentTimeMillis(); private Set users = new HashSet<>(); + private String typeOfCancerId; + private String pmid; + public String getOwner() { return owner; } @@ -104,4 +107,19 @@ public void setStudyViewFilter(StudyViewFilter studyViewFilter) { this.studyViewFilter = studyViewFilter; } + public String getTypeOfCancerId() { + return typeOfCancerId; + } + + public void setTypeOfCancerId(String typeOfCancerId) { + this.typeOfCancerId = typeOfCancerId; + } + + public String getPmid() { + return pmid; + } + + public void setPmid(String pmid) { + this.pmid = pmid; + } } diff --git a/src/main/resources/application.properties.EXAMPLE b/src/main/resources/application.properties.EXAMPLE index 50a440c0bca..82bfe4a07a5 100644 --- a/src/main/resources/application.properties.EXAMPLE +++ b/src/main/resources/application.properties.EXAMPLE @@ -239,6 +239,9 @@ session.service.url=https://cbioportal-session-service.herokuapp.com/session_ser #session.service.user= #session.service.password= +# Publishing Virtual Studies +#session.endpoint.publisher-api-key= + # disabled tabs, | delimited # possible values: cancer_types_summary, mutual_exclusivity, comparison, plots, mutations, co_expression, enrichments, survival, network, download, bookmark, IGV disabled_tabs= diff --git a/src/test/java/org/cbioportal/test/integration/PublicVirtualStudiesIntegrationTest.java b/src/test/java/org/cbioportal/test/integration/PublicVirtualStudiesIntegrationTest.java new file mode 100644 index 00000000000..e2c74996b75 --- /dev/null +++ b/src/test/java/org/cbioportal/test/integration/PublicVirtualStudiesIntegrationTest.java @@ -0,0 +1,238 @@ +package org.cbioportal.test.integration; + +import org.cbioportal.test.integration.security.ContainerConfig; +import org.cbioportal.web.parameter.StudyViewFilter; +import org.cbioportal.web.parameter.VirtualStudy; +import org.cbioportal.web.parameter.VirtualStudyData; +import org.cbioportal.web.parameter.VirtualStudySamples; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.cbioportal.test.integration.security.ContainerConfig.MyMysqlInitializer; +import static org.cbioportal.test.integration.security.ContainerConfig.PortInitializer; +import static org.cbioportal.test.integration.security.ContainerConfig.SESSION_SERVICE_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT +) +@TestPropertySource( + properties = { + "authenticate=false", + "session.endpoint.publisher-api-key=this-is-a-secret", + "session.service.url=http://localhost:" + SESSION_SERVICE_PORT + "/api/sessions/public_portal/", + // DB settings (also see MysqlInitializer) + "spring.datasource.driverClassName=com.mysql.jdbc.Driver", + "spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect", + } +) +@ContextConfiguration(initializers = { + MyMysqlInitializer.class, + PortInitializer.class +}) +@DirtiesContext +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class PublicVirtualStudiesIntegrationTest extends ContainerConfig { + + static final String CBIO_URL = String.format("http://localhost:%d", CBIO_PORT); + + static final HttpHeaders jsonContentType = new HttpHeaders() { + { + set("Content-Type", "application/json"); + } + }; + + static final HttpHeaders invalidKeyContainingHeaders = new HttpHeaders() { + { + set("X-PUBLISHER-API-KEY", "this-is-not-valid-key"); + } + }; + + static final HttpHeaders validKeyContainingHeaders = new HttpHeaders() { + { + set("X-PUBLISHER-API-KEY", "this-is-a-secret"); + } + }; + + static final ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() { + }; + + static String virtualStudyId; + + static final VirtualStudyData virtualStudyDataToSave = createTestVsData(); + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void test1NoPublicVirtualStudiesAtTheBeginning() { + ResponseEntity> response1 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies", + HttpMethod.GET, + null, + typeRef); + + assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response1.getBody()).isEmpty(); + } + + @Test + public void test2CreateVirtualStudy() { + + ResponseEntity response2 = restTemplate.exchange( + CBIO_URL + "/api/session/virtual_study", + HttpMethod.POST, + new HttpEntity<>(virtualStudyDataToSave, jsonContentType), + VirtualStudy.class); + assertThat(response2.getStatusCode().is2xxSuccessful()).isTrue(); + VirtualStudy savedVs = response2.getBody(); + assertThat(savedVs).isNotNull().hasFieldOrProperty("id").isNotNull(); + virtualStudyId = savedVs.getId(); + } + + @Test + public void test2_1UnPublishVirtualStudyFails() { + ResponseEntity response = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + new HttpEntity<>(null, validKeyContainingHeaders), + Object.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void test3PublishVirtualStudy() { + String url = CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId + "?typeOfCancerId=acc&pmid=12345"; + ResponseEntity response3 = restTemplate.exchange( + url, + HttpMethod.POST, + null, + Void.class); + assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + + response3 = restTemplate.exchange( + url, + HttpMethod.POST, + new HttpEntity<>(null, invalidKeyContainingHeaders), + Void.class); + assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + + response3 = restTemplate.exchange( + url, + HttpMethod.POST, + new HttpEntity<>(null, validKeyContainingHeaders), + Void.class); + assertThat(response3.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + public void test4ListJustPublishedStudy() { + ResponseEntity> response4 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies", + HttpMethod.GET, + null, + typeRef); + + assertThat(response4.getStatusCode().is2xxSuccessful()).isTrue(); + List virtualStudies = response4.getBody(); + assertThat(virtualStudies).isNotNull().hasSize(1); + VirtualStudy virtualStudy = virtualStudies.get(0); + VirtualStudyData virtualStudyData = virtualStudy.getData(); + assertThat(virtualStudyData) + .hasFieldOrPropertyWithValue("name", virtualStudyDataToSave.getName()) + .hasFieldOrPropertyWithValue("description", virtualStudyDataToSave.getDescription()) + .hasFieldOrPropertyWithValue("typeOfCancerId", "acc") + .hasFieldOrPropertyWithValue("pmid", "12345"); + assertThat(virtualStudyData.getStudies()).hasSize(2); + assertThat(virtualStudyData.getStudyViewFilter()).isNotNull(); + } + + @Test + public void test5UnpublishVirtualStudy() { + ResponseEntity response5 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + null, + Void.class); + assertThat(response5.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + + response5 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + new HttpEntity<>(null, invalidKeyContainingHeaders), + Void.class); + assertThat(response5.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + + response5 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + new HttpEntity<>(null, validKeyContainingHeaders), + Void.class); + assertThat(response5.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + public void test6NoPublicVirtualStudiesAfterRemoval() { + ResponseEntity> response6 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies", + HttpMethod.GET, + null, + typeRef); + + assertThat(response6.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response6.getBody()).isEmpty(); + } + + @Test + public void test7UnpublishedVirtualStudyExists() { + ResponseEntity response = restTemplate.exchange( + CBIO_URL + "/api/session/virtual_study/" + virtualStudyId, + HttpMethod.GET, + null, + VirtualStudy.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + VirtualStudy body = response.getBody(); + assertThat(body).isNotNull(); + } + + static VirtualStudyData createTestVsData() { + VirtualStudyData data = new VirtualStudyData(); + data.setName("test virtual study name"); + data.setDescription("test virtual study description"); + VirtualStudySamples study1 = new VirtualStudySamples(); + study1.setId("study_tcga_pub"); + study1.setSamples(Set.of("TCGA-A1-A0SB-01", "TCGA-A1-A0SJ-01")); + VirtualStudySamples study2 = new VirtualStudySamples(); + study2.setId("acc_tcga"); + study2.setSamples(Set.of("TCGA-XX-0800-01")); + Set studies = Set.of( + study1, + study2 + ); + data.setStudies(studies); + StudyViewFilter studyViewFilter = new StudyViewFilter(); + studyViewFilter.setStudyIds(List.of("study_tcga_pub", "acc_tcga")); + data.setStudyViewFilter(studyViewFilter); + return data; + } + +} \ No newline at end of file