diff --git a/src/main/java/org/sagebionetworks/bridge/models/apps/Exporter3Configuration.java b/src/main/java/org/sagebionetworks/bridge/models/apps/Exporter3Configuration.java index ab41cfa31..43e0a0d2f 100644 --- a/src/main/java/org/sagebionetworks/bridge/models/apps/Exporter3Configuration.java +++ b/src/main/java/org/sagebionetworks/bridge/models/apps/Exporter3Configuration.java @@ -13,6 +13,7 @@ public final class Exporter3Configuration { private String projectId; private String rawDataFolderId; private Long storageLocationId; + private String wikiPageId; /** Helper method that returns true if all configuration attributes are specified. */ public boolean isConfigured() { @@ -112,11 +113,19 @@ public void setStorageLocationId(Long storageLocationId) { this.storageLocationId = storageLocationId; } + public String getWikiPageId() { + return wikiPageId; + } + + public void setWikiPageId(String wikiPageId) { + this.wikiPageId = wikiPageId; + } + @Override public int hashCode() { return Objects.hash(createStudyNotificationTopicArn, dataAccessTeamId, exportNotificationTopicArn, participantVersionDemographicsTableId, participantVersionDemographicsViewId, participantVersionTableId, projectId, rawDataFolderId, - storageLocationId); + storageLocationId, wikiPageId); } @Override @@ -135,7 +144,8 @@ public boolean equals(Object obj) { && Objects.equals(participantVersionDemographicsViewId, other.participantVersionDemographicsViewId) && Objects.equals(participantVersionTableId, other.participantVersionTableId) && Objects.equals(projectId, other.projectId) && Objects.equals(rawDataFolderId, other.rawDataFolderId) - && Objects.equals(storageLocationId, other.storageLocationId); + && Objects.equals(storageLocationId, other.storageLocationId) + && Objects.equals(wikiPageId, other.wikiPageId); } @Override @@ -145,6 +155,6 @@ public String toString() { + participantVersionDemographicsTableId + ", participantVersionDemographicsViewId=" + participantVersionDemographicsViewId + ", participantVersionTableId=" + participantVersionTableId + ", projectId=" + projectId + ", rawDataFolderId=" + rawDataFolderId + ", storageLocationId=" - + storageLocationId + "]"; + + storageLocationId + ", wikiPageId=" + wikiPageId + "]"; } } diff --git a/src/main/java/org/sagebionetworks/bridge/services/Exporter3Service.java b/src/main/java/org/sagebionetworks/bridge/services/Exporter3Service.java index b889a95aa..9f0fd25aa 100644 --- a/src/main/java/org/sagebionetworks/bridge/services/Exporter3Service.java +++ b/src/main/java/org/sagebionetworks/bridge/services/Exporter3Service.java @@ -1,5 +1,6 @@ package org.sagebionetworks.bridge.services; +import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; @@ -20,20 +21,31 @@ import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.model.SendMessageResult; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.io.CharSink; +import com.google.common.io.FileWriteMode; +import com.google.common.io.Files; import org.apache.commons.lang3.StringUtils; +import org.sagebionetworks.client.SynapseClient; import org.sagebionetworks.client.exceptions.SynapseException; import org.sagebionetworks.repo.model.Folder; +import org.sagebionetworks.repo.model.ObjectType; import org.sagebionetworks.repo.model.Project; import org.sagebionetworks.repo.model.Team; +import org.sagebionetworks.repo.model.dao.WikiPageKey; +import org.sagebionetworks.repo.model.dao.WikiPageKeyHelper; +import org.sagebionetworks.repo.model.file.CloudProviderFileHandleInterface; import org.sagebionetworks.repo.model.annotation.v2.AnnotationsValue; import org.sagebionetworks.repo.model.annotation.v2.AnnotationsValueType; import org.sagebionetworks.repo.model.project.ExternalS3StorageLocationSetting; import org.sagebionetworks.repo.model.table.ColumnModel; import org.sagebionetworks.repo.model.table.ColumnType; import org.sagebionetworks.repo.model.table.EntityView; +import org.sagebionetworks.repo.model.v2.wiki.V2WikiPage; import org.sagebionetworks.repo.model.table.MaterializedView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,6 +78,8 @@ import org.sagebionetworks.bridge.models.exporter.ExporterSubscriptionRequest; import org.sagebionetworks.bridge.models.exporter.ExporterSubscriptionResult; import org.sagebionetworks.bridge.models.healthdata.HealthDataRecordEx3; +import org.sagebionetworks.bridge.models.schedules2.Schedule2; +import org.sagebionetworks.bridge.models.schedules2.timelines.Timeline; import org.sagebionetworks.bridge.models.studies.Study; import org.sagebionetworks.bridge.models.upload.Upload; import org.sagebionetworks.bridge.models.worker.Exporter3Request; @@ -265,6 +279,9 @@ public class Exporter3Service { private AmazonSQS sqsClient; private StudyService studyService; private SynapseHelper synapseHelper; + private Schedule2Service schedule2Service; + private SynapseClient synapseClient; + private FileService fileService; @Autowired public final void setConfig(BridgeConfig config) { @@ -340,11 +357,26 @@ final void setStudyService(StudyService studyService) { this.studyService = studyService; } + @Resource(name="exporterSynapseClient") + public final void setSynapseClient(SynapseClient synapseClient) { + this.synapseClient = synapseClient; + } + @Resource(name="exporterSynapseHelper") public final void setSynapseHelper(SynapseHelper synapseHelper) { this.synapseHelper = synapseHelper; } + @Autowired + final void setSchedule2Service(Schedule2Service schedule2Service) { + this.schedule2Service = schedule2Service; + } + + @Autowired + public final void setFileService(FileService fileService) { + this.fileService = fileService; + } + /** * Initializes configs and Synapse resources for Exporter 3.0. Note that if any config already exists, this API * will simply ignore them. This allows for two notable scenarios @@ -438,7 +470,6 @@ public Exporter3Configuration initExporter3ForStudy(String appId, String studyId sendNotification(appId, studyId, "create study", createStudyNotificationTopicArn, notification); } } - return ex3Config; } @@ -934,4 +965,55 @@ private void exportUpload(String appId, String recordId) { LOG.info("Sent export request for app " + appId + " record " + recordId + "; received message ID=" + sqsResult.getMessageId()); } + + // Export timeline from Bridge to Synapse (Some researchers only have access to Synapse, not Bridge, + // so they need access to the Timeline information.). + public Exporter3Configuration exportTimelineForStudy(String appId, String studyId) throws BridgeSynapseException, + SynapseException, IOException{ + // Get Timeline to export for the study. + Study study = studyService.getStudy(appId, studyId, true); + if (study.getScheduleGuid() == null) { + throw new EntityNotFoundException(Schedule2.class); + } + Timeline timeline = schedule2Service.getTimelineForSchedule(appId, + study.getScheduleGuid()); + + // Check Synapse + synapseHelper.checkSynapseWritableOrThrow(); + + // If Exporter3 is not enabled for study, initiate Exporter3 for Study; + if (!study.isExporter3Enabled()) { + initExporter3ForStudy(appId, studyId); + study = studyService.getStudy(appId, studyId, true); + } + Exporter3Configuration exporter3Config = study.getExporter3Configuration(); + + // Export the study's Timeline to Synapse as a wiki page in JSON format. + // If we have an IOException, the wiki page will almost certainly fail to upload. Instead of catching the exception, + // we just let the exception get thrown. + JsonNode node = BridgeObjectMapper.get().valueToTree(timeline); + File outputFile = File.createTempFile("timelineFor" + studyId, ".txt"); + CharSink charSink = Files.asCharSink(outputFile, Charsets.UTF_8, FileWriteMode.APPEND); + charSink.write(node.toString()); + + CloudProviderFileHandleInterface markdown = synapseClient.multipartUpload(outputFile, + exporter3Config.getStorageLocationId(), false, false); + + // If first time exporting the timeline for the study, create a new wiki page in Synapse; + // If wiki page already exists, update the existing wiki page. + if (exporter3Config.getWikiPageId() == null) { + V2WikiPage wiki = new V2WikiPage(); + wiki.setTitle("Exported Timeline for " + studyId); + wiki.setMarkdownFileHandleId(markdown.getId()); + wiki = synapseClient.createV2WikiPage(exporter3Config.getProjectId(), ObjectType.ENTITY, wiki); + exporter3Config.setWikiPageId(wiki.getId()); + studyService.updateStudy(appId, study); + } else { + WikiPageKey key = WikiPageKeyHelper.createWikiPageKey(exporter3Config.getProjectId(), ObjectType.ENTITY, exporter3Config.getWikiPageId()); + V2WikiPage getWiki = synapseClient.getV2WikiPage(key); + getWiki.setMarkdownFileHandleId(markdown.getId()); + synapseClient.updateV2WikiPage(exporter3Config.getProjectId(), ObjectType.ENTITY, getWiki); + } + return exporter3Config; + } } diff --git a/src/main/java/org/sagebionetworks/bridge/spring/controllers/Exporter3Controller.java b/src/main/java/org/sagebionetworks/bridge/spring/controllers/Exporter3Controller.java index 9e53a0952..3a34fa458 100644 --- a/src/main/java/org/sagebionetworks/bridge/spring/controllers/Exporter3Controller.java +++ b/src/main/java/org/sagebionetworks/bridge/spring/controllers/Exporter3Controller.java @@ -96,6 +96,15 @@ public Exporter3Configuration initExporter3ForStudy(@PathVariable String studyId return exporter3Service.initExporter3ForStudy(session.getAppId(), studyId); } + @PostMapping("/v5/studies/{studyId}/timeline/export") + @ResponseStatus(HttpStatus.CREATED) + public Exporter3Configuration exportTimelineForStudy(@PathVariable String studyId) + throws BridgeSynapseException, SynapseException, IOException { + UserSession session = getAuthenticatedSession(STUDY_DESIGNER, DEVELOPER); + CAN_UPDATE_STUDIES.checkAndThrow(STUDY_ID, studyId); + return exporter3Service.exportTimelineForStudy(session.getAppId(), studyId); + } + /** Subscribe to be notified when health data is exported to the study-specific Synapse project. */ @PostMapping(path = "/v5/studies/{studyId}/exporter3/notifications/export/subscribe") @ResponseStatus(HttpStatus.CREATED) diff --git a/src/test/java/org/sagebionetworks/bridge/services/Exporter3ServiceTest.java b/src/test/java/org/sagebionetworks/bridge/services/Exporter3ServiceTest.java index 714903ef5..596481451 100644 --- a/src/test/java/org/sagebionetworks/bridge/services/Exporter3ServiceTest.java +++ b/src/test/java/org/sagebionetworks/bridge/services/Exporter3ServiceTest.java @@ -23,6 +23,7 @@ import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -38,27 +39,37 @@ import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.model.SendMessageResult; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.io.CharSource; +import com.google.common.io.Files; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import org.sagebionetworks.client.SynapseClient; import org.sagebionetworks.client.exceptions.SynapseException; import org.sagebionetworks.client.exceptions.UnknownSynapseServerException; import org.sagebionetworks.repo.model.Entity; import org.sagebionetworks.repo.model.Folder; +import org.sagebionetworks.repo.model.ObjectType; import org.sagebionetworks.repo.model.Project; import org.sagebionetworks.repo.model.Team; +import org.sagebionetworks.repo.model.dao.WikiPageKey; +import org.sagebionetworks.repo.model.file.CloudProviderFileHandleInterface; +import org.sagebionetworks.repo.model.file.S3FileHandle; import org.sagebionetworks.repo.model.annotation.v2.AnnotationsValue; import org.sagebionetworks.repo.model.annotation.v2.AnnotationsValueType; import org.sagebionetworks.repo.model.project.ExternalS3StorageLocationSetting; import org.sagebionetworks.repo.model.table.EntityView; import org.sagebionetworks.repo.model.table.MaterializedView; import org.sagebionetworks.repo.model.table.TableEntity; +import org.sagebionetworks.repo.model.v2.wiki.V2WikiPage; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -88,6 +99,8 @@ import org.sagebionetworks.bridge.models.exporter.ExporterSubscriptionRequest; import org.sagebionetworks.bridge.models.exporter.ExporterSubscriptionResult; import org.sagebionetworks.bridge.models.healthdata.HealthDataRecordEx3; +import org.sagebionetworks.bridge.models.schedules2.Schedule2; +import org.sagebionetworks.bridge.models.schedules2.timelines.Timeline; import org.sagebionetworks.bridge.models.studies.Study; import org.sagebionetworks.bridge.models.upload.Upload; import org.sagebionetworks.bridge.models.worker.Exporter3Request; @@ -169,6 +182,7 @@ public class Exporter3ServiceTest { private App app; private Study study; + private Schedule2 schedule; @Mock private AccountService mockAccountService; @@ -200,6 +214,12 @@ public class Exporter3ServiceTest { @Mock private SynapseHelper mockSynapseHelper; + @Mock + private SynapseClient mockSynapseClient; + + @Mock + private Schedule2Service mockSchedule2Service; + @InjectMocks @Spy private Exporter3Service exporter3Service; @@ -226,6 +246,11 @@ public void before() { study.setName(APP_NAME); when(mockStudyService.getStudy(eq(TestConstants.TEST_APP_ID), eq(TestConstants.TEST_STUDY_ID), anyBoolean())) .thenReturn(study); + + // Mock Schedule + schedule = new Schedule2(); + schedule.setAppId(TEST_APP_ID); + schedule.setGuid(TestConstants.SCHEDULE_GUID); } @AfterClass @@ -555,8 +580,7 @@ private void mockSynapseResourceCreation() throws Exception { EntityView trackingView = new EntityView(); trackingView.setScopeIds(new ArrayList<>()); - when(mockSynapseHelper.getEntityWithRetry(SYNAPSE_TRACKING_VIEW_ID, EntityView.class)) - .thenReturn(trackingView); + when(mockSynapseHelper.getEntityWithRetry(SYNAPSE_TRACKING_VIEW_ID, EntityView.class)).thenReturn(trackingView); TableEntity createdParticipantVersionsTable = new TableEntity(); createdParticipantVersionsTable.setId(PARTICIPANT_VERSION_TABLE_ID); @@ -1550,6 +1574,162 @@ public void completeUpload_NoAccount() throws Exception { verifyZeroInteractions(mockHealthDataEx3Service, mockSqsClient); } + @Test + public void exportTimelineForStudy_1stTime() throws Exception { + // Setup Study. + study.setScheduleGuid(TestConstants.SCHEDULE_GUID); + + // Study has no exporter3config. + study.setExporter3Configuration(null); + study.setExporter3Enabled(false); + + // Mock Synapse resource creation. + mockSynapseResourceCreation(); + + // Mock Timeline + Timeline.Builder builder = new Timeline.Builder(); + Timeline timeline = builder.withSchedule(schedule).build(); + when(mockSchedule2Service.getTimelineForSchedule(eq(TEST_APP_ID), eq(TestConstants.SCHEDULE_GUID))) + .thenReturn(timeline); + + // Mock FileHandle + CloudProviderFileHandleInterface markdown = new S3FileHandle(); + markdown.setStorageLocationId(STORAGE_LOCATION_ID); + markdown.setId("exportedTimelineFor" + TestConstants.TEST_STUDY_ID); + markdown.setFileName("timelineFor" + TestConstants.TEST_STUDY_ID + ".txt"); + ArgumentCaptor fileToUploadCaptor = ArgumentCaptor.forClass(File.class); + when(mockSynapseClient.multipartUpload(fileToUploadCaptor.capture(),any(), eq(false), eq(false))) + .thenReturn(markdown); + + // Setup Wiki + V2WikiPage wiki = new V2WikiPage(); + wiki.setId("wikiFor" + TestConstants.TEST_STUDY_ID); + wiki.setTitle("Exported Timeline for " + TestConstants.TEST_STUDY_ID); + wiki.setMarkdownFileHandleId("exportedTimelineFor" + TestConstants.TEST_STUDY_ID); + ArgumentCaptor wikiToCreateCaptor = ArgumentCaptor.forClass(V2WikiPage.class); + when(mockSynapseClient.createV2WikiPage(any(), eq(ObjectType.ENTITY), wikiToCreateCaptor.capture())).thenReturn(wiki); + + // Execute and verify output. + Exporter3Configuration ex3Config = exporter3Service.exportTimelineForStudy(TEST_APP_ID, + TestConstants.TEST_STUDY_ID); + assertEquals(ex3Config, study.getExporter3Configuration()); + assertEquals(ex3Config.getWikiPageId(), wiki.getId()); + verifyEx3ConfigAndSynapse(ex3Config, TestConstants.TEST_APP_ID + '/' + + TestConstants.TEST_STUDY_ID); + + // Verify call to studyService three times: one in initExporter3ForStudy & two in exportTimelineForStudy + // (Need to get study again after call initExporter3ForStudy). + verify(mockStudyService, times(3)).getStudy(TEST_APP_ID, TEST_STUDY_ID, true); + + // Verify call to studyService twice: one in initExporter3ForStudy & one in exportTimelineForStudy. + verify(mockStudyService, times(2)).updateStudy(TEST_APP_ID, study); + + // Verify call to schedule2Service. + verify(mockSchedule2Service).getTimelineForSchedule(TEST_APP_ID, TestConstants.SCHEDULE_GUID); + + // Verify call to SynapseHelper. + verify(mockSynapseHelper).checkSynapseWritableOrThrow(); + + // Verify call to SynapseClient. + verify(mockSynapseClient).createV2WikiPage(eq(study.getExporter3Configuration().getProjectId()), eq(ObjectType.ENTITY), wikiToCreateCaptor.capture()); + verify(mockSynapseClient).multipartUpload(fileToUploadCaptor.capture(),eq(study.getExporter3Configuration().getStorageLocationId()), eq(false), eq(false)); + + // Verify file content + JsonNode node = BridgeObjectMapper.get().valueToTree(timeline); + String expectedFileContent = node.toString(); + File capturedFile = fileToUploadCaptor.getValue(); + CharSource charSource = Files.asCharSource(capturedFile, Charsets.UTF_8); + String content = charSource.read(); + assertEquals(content, expectedFileContent); + + // Verify WikiPage + V2WikiPage capturedWikiPage = wikiToCreateCaptor.getValue(); + assertEquals(capturedWikiPage.getTitle(), wiki.getTitle()); + assertEquals(capturedWikiPage.getMarkdownFileHandleId(), wiki.getMarkdownFileHandleId()); + + // Verify markdown + assertTrue((capturedFile.getName().replace(".txt", "") + .contains(markdown.getFileName().replace(".txt", "")))); + } + + @Test + public void exportTimelineForStudy_2ndTime() throws Exception { + // Setup Study. + study.setScheduleGuid(TestConstants.SCHEDULE_GUID); + + // Setup existing S3FileHandle + CloudProviderFileHandleInterface existingMarkdown = new S3FileHandle(); + existingMarkdown.setStorageLocationId(STORAGE_LOCATION_ID); + existingMarkdown.setId("1stExportedTimelineFor" + TestConstants.TEST_STUDY_ID); + + // Setup Wiki + V2WikiPage wiki = new V2WikiPage(); + wiki.setId("wikiFor" + TestConstants.TEST_STUDY_ID); + wiki.setTitle("Exported Timeline for " + TestConstants.TEST_STUDY_ID); + wiki.setMarkdownFileHandleId(existingMarkdown.getId()); + + // Setup Exporter3Config + Exporter3Configuration exporter3Config = new Exporter3Configuration(); + exporter3Config.setWikiPageId(wiki.getId()); + exporter3Config.setStorageLocationId(STORAGE_LOCATION_ID); + exporter3Config.setProjectId(PROJECT_ID); + + // Setup exporter3config for study. + study.setExporter3Enabled(true); + study.setExporter3Configuration(exporter3Config); + + // Mock Synapse resource creation. + mockSynapseResourceCreation(); + + // Mock Timeline + Timeline.Builder builder = new Timeline.Builder(); + Timeline timeline = builder.withSchedule(schedule).build(); + when(mockSchedule2Service.getTimelineForSchedule(eq(TEST_APP_ID), eq(TestConstants.SCHEDULE_GUID))) + .thenReturn(timeline); + + // Mock S3FileHandle + CloudProviderFileHandleInterface markdown = new S3FileHandle(); + markdown.setStorageLocationId(8888L); + markdown.setId("2ndExportedTimelineFor" + TestConstants.TEST_STUDY_ID); + when(mockSynapseClient.multipartUpload(any(File.class), eq(STORAGE_LOCATION_ID), eq(false), eq(false))).thenReturn(markdown); + + // Mock WikiPageKey + WikiPageKey key = new WikiPageKey(); + key.setOwnerObjectId(PROJECT_ID); + key.setOwnerObjectType(ObjectType.ENTITY); + key.setWikiPageId("wikiFor" + TestConstants.TEST_STUDY_ID); + when(mockSynapseClient.getV2WikiPage(any(WikiPageKey.class))).thenReturn(wiki); + + // Execute and verify output. + Exporter3Configuration returnedEx3Config = exporter3Service.exportTimelineForStudy(TEST_APP_ID, + TestConstants.TEST_STUDY_ID); + + assertEquals(returnedEx3Config.getWikiPageId(), "wikiFor" + TestConstants.TEST_STUDY_ID); + assertEquals(returnedEx3Config, study.getExporter3Configuration()); + + // Verify call to schedule2Service. + verify(mockSchedule2Service, times(1)).getTimelineForSchedule(TEST_APP_ID, TestConstants.SCHEDULE_GUID); + + // Verify call to SynapseHelper. + verify(mockSynapseHelper, times(1)).checkSynapseWritableOrThrow(); + + // Verify call to SynapseClient. + ArgumentCaptor WikiPageKeyCaptor = ArgumentCaptor.forClass(WikiPageKey.class); + verify(mockSynapseClient).getV2WikiPage(WikiPageKeyCaptor.capture()); + ArgumentCaptor wikiToCreateCaptor = ArgumentCaptor.forClass(V2WikiPage.class); + verify(mockSynapseClient).updateV2WikiPage(eq(study.getExporter3Configuration().getProjectId()), eq(ObjectType.ENTITY), wikiToCreateCaptor.capture()); + + // Verify WikiPageKey + WikiPageKey capturedWikiPageKey = WikiPageKeyCaptor.getValue(); + assertEquals(capturedWikiPageKey.getOwnerObjectId(), key.getOwnerObjectId()); + assertEquals(capturedWikiPageKey.getOwnerObjectType(), key.getOwnerObjectType()); + assertEquals(capturedWikiPageKey.getWikiPageId(), key.getWikiPageId()); + + // Verify updated WikiPage + V2WikiPage capturedWikiPage = wikiToCreateCaptor.getValue(); + assertEquals(capturedWikiPage.getMarkdownFileHandleId(), markdown.getId()); + } + // checks study and app id annotations are not added when the project exists @Test public void studyMetadataProjectExists() throws BridgeSynapseException, IOException, SynapseException { diff --git a/src/test/java/org/sagebionetworks/bridge/spring/controllers/Exporter3ControllerTest.java b/src/test/java/org/sagebionetworks/bridge/spring/controllers/Exporter3ControllerTest.java index ba5379977..d778b146b 100644 --- a/src/test/java/org/sagebionetworks/bridge/spring/controllers/Exporter3ControllerTest.java +++ b/src/test/java/org/sagebionetworks/bridge/spring/controllers/Exporter3ControllerTest.java @@ -202,6 +202,28 @@ public void initExporter3ForStudy() throws Exception { } @Test + public void exportTimelineForStudy() throws Exception { + // Set up request context. + RequestContext.set(new RequestContext.Builder() + .withOrgSponsoredStudies(ImmutableSet.of(TEST_STUDY_ID)) + .withCallerRoles(ImmutableSet.of(STUDY_DESIGNER)) + .build()); + + // Mock session. + UserSession mockSession = new UserSession(); + mockSession.setAppId(TestConstants.TEST_APP_ID); + doReturn(mockSession).when(controller).getAuthenticatedSession(STUDY_DESIGNER, DEVELOPER); + + // Mock service. + Exporter3Configuration ex3Config = new Exporter3Configuration(); + when(mockSvc.exportTimelineForStudy(TestConstants.TEST_APP_ID, TestConstants.TEST_STUDY_ID)) + .thenReturn(ex3Config); + + // Execute. + Exporter3Configuration retVal = controller.exportTimelineForStudy(TestConstants.TEST_STUDY_ID); + assertSame(retVal, ex3Config); + } + public void subscribeToExportNotificationsForStudy() throws Exception { // Set up request context. RequestContext.set(new RequestContext.Builder()