diff --git a/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1.java b/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1.java index cba4296f0..f76225222 100644 --- a/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1.java +++ b/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1.java @@ -13,6 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -26,6 +27,7 @@ */ @RestController @RequestMapping("/cpc") +@CrossOrigin public class CpcFileControllerV1 { private static final Logger API_LOG = LoggerFactory.getLogger(Constants.API_LOG); @@ -69,7 +71,7 @@ public ResponseEntity> getUnprocessedCpcPlusFiles() headers = {"Accept=" + Constants.V1_API_ACCEPT}) public ResponseEntity getFileById(@PathVariable("fileId") String fileId) throws IOException { - API_LOG.info("CPC+ file request received"); + API_LOG.info("CPC+ file retrieval request received"); if (blockCpcPlusApi()) { API_LOG.info("CPC+ file request blocked by feature flag"); @@ -78,7 +80,7 @@ public ResponseEntity getFileById(@PathVariable("fileId") S InputStreamResource content = cpcFileService.getFileById(fileId); - API_LOG.info("CPC+ file request succeeded"); + API_LOG.info("CPC+ file retrieval request succeeded"); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_XML); @@ -86,6 +88,29 @@ public ResponseEntity getFileById(@PathVariable("fileId") S return new ResponseEntity<>(content, httpHeaders, HttpStatus.OK); } + /** + * Updates a file's status to processed in the database + * + * @param fileId Identifier of the file needing to be updated + * @return Message if the file was updated or not + */ + @RequestMapping(method = RequestMethod.PUT, value = "/file/{fileId}", + headers = {"Accept=" + Constants.V1_API_ACCEPT} ) + public ResponseEntity markFileProcessed(@PathVariable("fileId") String fileId) { + if (blockCpcPlusApi()) { + API_LOG.info("CPC+ unprocessed files request blocked by feature flag"); + return new ResponseEntity<>(null, null, HttpStatus.FORBIDDEN); + } + + API_LOG.info("CPC+ update file as processed request received"); + String message = cpcFileService.processFileById(fileId); + API_LOG.info("CPC+ update file as processed request succeeded"); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + + return new ResponseEntity<>(message, httpHeaders, HttpStatus.OK); + } + /** * Checks whether the the CPC+ APIs should not be allowed to execute. * @@ -93,5 +118,6 @@ public ResponseEntity getFileById(@PathVariable("fileId") S */ private boolean blockCpcPlusApi() { return EnvironmentHelper.isPresent(Constants.NO_CPC_PLUS_API_ENV_VARIABLE); + } } diff --git a/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1.java b/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1.java index d4ca8ca73..353db775a 100644 --- a/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1.java +++ b/rest-api/src/main/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1.java @@ -1,5 +1,6 @@ package gov.cms.qpp.conversion.api.controllers.v1; +import gov.cms.qpp.conversion.api.exceptions.InvalidFileTypeException; import gov.cms.qpp.conversion.api.exceptions.NoFileInDatabaseException; import gov.cms.qpp.conversion.api.model.Constants; import gov.cms.qpp.conversion.api.services.AuditService; @@ -60,7 +61,7 @@ ResponseEntity handleQppValidationException(QppValidationException ex /** * "Catch" the {@link NoFileInDatabaseException}. - * Return the {@link AllErrors} with an HTTP status 422. + * Return the {@link AllErrors} with an HTTP status 404. * * @param exception The NoFileInDatabaseException that was "caught". * @return The NoFileInDatabaseException message @@ -74,6 +75,23 @@ ResponseEntity handleFileNotFoundException(NoFileInDatabaseException exc return new ResponseEntity<>(exception.getMessage(), httpHeaders, HttpStatus.NOT_FOUND); } + + /** + * "Catch" the {@link InvalidFileTypeException}. + * Return the {@link AllErrors} with an HTTP status 404. + * + * @param exception The InvalidFileTypeException that was "caught". + * @return The InvalidFileTypeException message + */ + @ExceptionHandler(InvalidFileTypeException.class) + @ResponseBody + ResponseEntity handleInvalidFileTypeException(InvalidFileTypeException exception) { + API_LOG.error("A file type error occurred", exception); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + + return new ResponseEntity<>(exception.getMessage(), httpHeaders, HttpStatus.NOT_FOUND); + } private ResponseEntity cope(TransformException exception) { HttpHeaders httpHeaders = new HttpHeaders(); diff --git a/rest-api/src/main/java/gov/cms/qpp/conversion/api/exceptions/InvalidFileTypeException.java b/rest-api/src/main/java/gov/cms/qpp/conversion/api/exceptions/InvalidFileTypeException.java new file mode 100644 index 000000000..d3b999ed0 --- /dev/null +++ b/rest-api/src/main/java/gov/cms/qpp/conversion/api/exceptions/InvalidFileTypeException.java @@ -0,0 +1,15 @@ +package gov.cms.qpp.conversion.api.exceptions; + +/** + * Exception for handling an invalid file type + */ +public class InvalidFileTypeException extends RuntimeException { + + /** + * Constructor to call RuntimeException + * @param message Error response + */ + public InvalidFileTypeException(String message) { + super(message); + } +} diff --git a/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileService.java b/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileService.java index 7f12e7bc4..955a8698d 100644 --- a/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileService.java +++ b/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileService.java @@ -25,4 +25,12 @@ public interface CpcFileService { * @throws IOException for invalid IOUtils usage */ InputStreamResource getFileById(String fileId) throws IOException; + + /** + * Marks a CPC File as processed by id + * + * @param fileId Identifier of the CPC+ file + * @return Success or failure message + */ + String processFileById(String fileId); } diff --git a/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImpl.java b/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImpl.java index b21ac0a5d..9d54e8f0b 100644 --- a/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImpl.java +++ b/rest-api/src/main/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImpl.java @@ -1,15 +1,16 @@ package gov.cms.qpp.conversion.api.services; +import gov.cms.qpp.conversion.api.exceptions.InvalidFileTypeException; import gov.cms.qpp.conversion.api.exceptions.NoFileInDatabaseException; import gov.cms.qpp.conversion.api.model.Metadata; import gov.cms.qpp.conversion.api.model.UnprocessedCpcFileData; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.stream.Collectors; - /** * Service for handling Cpc File meta data */ @@ -17,6 +18,8 @@ public class CpcFileServiceImpl implements CpcFileService { public static final String FILE_NOT_FOUND = "File not found!"; + protected static final String INVALID_FILE = "The file was not a CPC+ file."; + protected static final String FILE_FOUND = "The file was found and will be updated as processed."; @Autowired private DbService dbService; @@ -44,13 +47,35 @@ public List getUnprocessedCpcPlusFiles() { */ public InputStreamResource getFileById(String fileId) { Metadata metadata = dbService.getMetadataById(fileId); - if (metadata != null && metadata.getCpc() != null && !metadata.getCpcProcessed()) { + if (isAnUnprocessedCpcFile(metadata)) { return new InputStreamResource(storageService.getFileByLocationId(metadata.getSubmissionLocator())); } else { throw new NoFileInDatabaseException(FILE_NOT_FOUND); } } + /** + * Process to ensure the file is an unprocessed cpc+ file and marks the file as processed + * + * @param fileId Identifier of the CPC+ file + * @return Success or failure message. + */ + public String processFileById(String fileId) { + Metadata metadata = dbService.getMetadataById(fileId); + if (metadata == null) { + throw new NoFileInDatabaseException(FILE_NOT_FOUND); + } else if (metadata.getCpc() == null) { + throw new InvalidFileTypeException(INVALID_FILE); + } else if (metadata.getCpcProcessed()) { + return FILE_FOUND; + } else { + metadata.setCpcProcessed(true); + CompletableFuture metadataFuture = dbService.write(metadata); + metadataFuture.join(); + return FILE_FOUND; + } + } + /** * Service to transform a {@link Metadata} list into the {@link UnprocessedCpcFileData} * @@ -60,4 +85,14 @@ public InputStreamResource getFileById(String fileId) { private List transformMetaDataToUnprocessedCpcFileData(List metadataList) { return metadataList.stream().map(UnprocessedCpcFileData::new).collect(Collectors.toList()); } + + /** + * Determines if the file is unprocessed and is CPC+ + * + * @param metadata Data to be determined valid or invalid + * @return result of the check + */ + private boolean isAnUnprocessedCpcFile(Metadata metadata) { + return metadata != null && metadata.getCpc() != null && !metadata.getCpcProcessed(); + } } diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1Test.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1Test.java index 091210208..541a95588 100644 --- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1Test.java +++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/CpcFileControllerV1Test.java @@ -25,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -71,6 +72,28 @@ void testGetFileById() throws IOException { .isEqualTo("1234"); } + @Test + void testMarkFileAsProcessedReturnsSuccess() { + when(cpcFileService.processFileById(anyString())).thenReturn("success!"); + + ResponseEntity response = cpcFileControllerV1.markFileProcessed("meep"); + + verify(cpcFileService, times(1)).processFileById("meep"); + + assertThat(response.getBody()).isEqualTo("success!"); + } + + @Test + void testMarkFileAsProcessedHttpStatusOk() { + when(cpcFileService.processFileById(anyString())).thenReturn("success!"); + + ResponseEntity response = cpcFileControllerV1.markFileProcessed("meep"); + + verify(cpcFileService, times(1)).processFileById("meep"); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + @Test void testEndpoint1WithFeatureFlagDisabled() { System.setProperty(Constants.NO_CPC_PLUS_API_ENV_VARIABLE, "trueOrWhatever"); @@ -91,6 +114,16 @@ void testEndpoint2WithFeatureFlagDisabled() throws IOException { assertThat(cpcResponse.getBody()).isNull(); } + @Test + void testEndpoint3WithFeatureFlagDisabled() throws IOException { + System.setProperty(Constants.NO_CPC_PLUS_API_ENV_VARIABLE, "trueOrWhatever"); + + ResponseEntity cpcResponse = cpcFileControllerV1.markFileProcessed("meep"); + + assertThat(cpcResponse.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(cpcResponse.getBody()).isNull(); + } + List createMockedUnprocessedDataList() { Metadata metadata = new Metadata(); metadata.setSubmissionLocator("Test"); diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1Test.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1Test.java index ceaed4bad..242bc3582 100644 --- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1Test.java +++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/controllers/v1/ExceptionHandlerControllerV1Test.java @@ -1,14 +1,18 @@ package gov.cms.qpp.conversion.api.controllers.v1; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static org.mockito.ArgumentMatchers.any; -import static org.powermock.api.mockito.PowerMockito.when; - +import gov.cms.qpp.conversion.Converter; +import gov.cms.qpp.conversion.PathSource; +import gov.cms.qpp.conversion.api.exceptions.InvalidFileTypeException; +import gov.cms.qpp.conversion.api.exceptions.NoFileInDatabaseException; +import gov.cms.qpp.conversion.api.services.AuditService; +import gov.cms.qpp.conversion.api.services.CpcFileServiceImpl; +import gov.cms.qpp.conversion.model.error.AllErrors; +import gov.cms.qpp.conversion.model.error.QppValidationException; +import gov.cms.qpp.conversion.model.error.TransformException; +import gov.cms.qpp.test.MockitoExtension; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,15 +23,10 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import gov.cms.qpp.conversion.Converter; -import gov.cms.qpp.conversion.PathSource; -import gov.cms.qpp.conversion.api.exceptions.NoFileInDatabaseException; -import gov.cms.qpp.conversion.api.services.AuditService; -import gov.cms.qpp.conversion.api.services.CpcFileServiceImpl; -import gov.cms.qpp.conversion.model.error.AllErrors; -import gov.cms.qpp.conversion.model.error.QppValidationException; -import gov.cms.qpp.conversion.model.error.TransformException; -import gov.cms.qpp.test.MockitoExtension; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.mockito.ArgumentMatchers.any; +import static org.powermock.api.mockito.PowerMockito.when; @ExtendWith(MockitoExtension.class) class ExceptionHandlerControllerV1Test { @@ -149,4 +148,36 @@ void testFileNotFoundExceptionBody() { ResponseEntity responseEntity = objectUnderTest.handleFileNotFoundException(exception); assertThat(responseEntity.getBody()).isEqualTo(CpcFileServiceImpl.FILE_NOT_FOUND); } + + @Test + void testInvalidFileTypeExceptionStatusCode() { + InvalidFileTypeException exception = + new InvalidFileTypeException(CpcFileServiceImpl.FILE_NOT_FOUND); + + ResponseEntity responseEntity = objectUnderTest.handleInvalidFileTypeException(exception); + + assertWithMessage("The response entity's status code must be 422.") + .that(responseEntity.getStatusCode()) + .isEquivalentAccordingToCompareTo(HttpStatus.NOT_FOUND); + } + + @Test + void testInvalidFileTypeExceptionHeaderContentType() { + InvalidFileTypeException exception = + new InvalidFileTypeException(CpcFileServiceImpl.FILE_NOT_FOUND); + + ResponseEntity responseEntity = objectUnderTest.handleInvalidFileTypeException(exception); + + assertThat(responseEntity.getHeaders().getContentType()) + .isEquivalentAccordingToCompareTo(MediaType.TEXT_PLAIN); + } + + @Test + void testInvalidFileTypeExceptionBody() { + InvalidFileTypeException exception = + new InvalidFileTypeException(CpcFileServiceImpl.FILE_NOT_FOUND); + + ResponseEntity responseEntity = objectUnderTest.handleInvalidFileTypeException(exception); + assertThat(responseEntity.getBody()).isEqualTo(CpcFileServiceImpl.FILE_NOT_FOUND); + } } \ No newline at end of file diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/exceptions/InvalidFileTypeExceptionTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/exceptions/InvalidFileTypeExceptionTest.java new file mode 100644 index 000000000..abf86b6d5 --- /dev/null +++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/exceptions/InvalidFileTypeExceptionTest.java @@ -0,0 +1,16 @@ +package gov.cms.qpp.conversion.api.exceptions; + +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; + +class InvalidFileTypeExceptionTest { + + @Test + void testConstructor() { + InvalidFileTypeException invalidFileTypeException = new InvalidFileTypeException("test"); + + + assertThat(invalidFileTypeException).hasMessageThat().isEqualTo("test"); + } +} diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/exceptions/NoFileInDatabaseException.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/exceptions/NoFileInDatabaseException.java new file mode 100644 index 000000000..ef7ee5993 --- /dev/null +++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/exceptions/NoFileInDatabaseException.java @@ -0,0 +1,16 @@ +package gov.cms.qpp.conversion.api.exceptions; + +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; + +class NoFileInDatabaseExceptionTest { + + @Test + void testConstructor() { + NoFileInDatabaseException noFileInDatabaseException = new NoFileInDatabaseException("test"); + + + assertThat(noFileInDatabaseException).hasMessageThat().isEqualTo("test"); + } +} diff --git a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImplTest.java b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImplTest.java index 900542caf..f71bda72d 100644 --- a/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImplTest.java +++ b/rest-api/src/test/java/gov/cms/qpp/conversion/api/services/CpcFileServiceImplTest.java @@ -1,8 +1,16 @@ package gov.cms.qpp.conversion.api.services; +import gov.cms.qpp.conversion.api.exceptions.InvalidFileTypeException; import gov.cms.qpp.conversion.api.exceptions.NoFileInDatabaseException; import gov.cms.qpp.conversion.api.model.Metadata; import gov.cms.qpp.test.MockitoExtension; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,15 +20,9 @@ import org.mockito.Mock; import org.springframework.core.io.InputStreamResource; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -29,6 +31,8 @@ @ExtendWith(MockitoExtension.class) class CpcFileServiceImplTest { + private static final String MEEP = "meep"; + @InjectMocks private CpcFileServiceImpl objectUnderTest; @@ -105,6 +109,55 @@ void testGetFileByIdNoFile() throws IOException { assertThat(expectedException).hasMessageThat().isEqualTo(CpcFileServiceImpl.FILE_NOT_FOUND); } + @Test + void testProcessFileByIdSuccess() { + Metadata returnedData = buildFakeMetadata(true, false); + when(dbService.getMetadataById(anyString())).thenReturn(returnedData); + when(dbService.write(any(Metadata.class))).thenReturn(CompletableFuture.completedFuture(returnedData)); + + String message = objectUnderTest.processFileById(MEEP); + + verify(dbService, times(1)).getMetadataById(MEEP); + verify(dbService, times(1)).write(returnedData); + + assertThat(message).isEqualTo(CpcFileServiceImpl.FILE_FOUND); + } + + @Test + void testProcessFileByIdFileNotFound() { + when(dbService.getMetadataById(anyString())).thenReturn(null); + + NoFileInDatabaseException expectedException = assertThrows(NoFileInDatabaseException.class, () + -> objectUnderTest.processFileById("test")); + + verify(dbService, times(1)).getMetadataById(anyString()); + + assertThat(expectedException).hasMessageThat().isEqualTo(CpcFileServiceImpl.FILE_NOT_FOUND); + } + + @Test + void testProcessFileByIdWithMipsFile() { + when(dbService.getMetadataById(anyString())).thenReturn(buildFakeMetadata(false, false)); + + InvalidFileTypeException expectedException = assertThrows(InvalidFileTypeException.class, () + -> objectUnderTest.processFileById("test")); + + verify(dbService, times(1)).getMetadataById(anyString()); + + assertThat(expectedException).hasMessageThat().isEqualTo(CpcFileServiceImpl.INVALID_FILE); + } + + @Test + void testProcessFileByIdWithProcessedFile() { + when(dbService.getMetadataById(anyString())).thenReturn(buildFakeMetadata(true, true)); + + String response = objectUnderTest.processFileById("test"); + + verify(dbService, times(1)).getMetadataById(anyString()); + + assertThat(response).isEqualTo(CpcFileServiceImpl.FILE_FOUND); + } + Metadata buildFakeMetadata(boolean isCpc, boolean isCpcProcessed) { Metadata metadata = new Metadata(); metadata.setCpc(isCpc ? "CPC_26" : null);