diff --git a/postman/Opal Print.postman_collection.json b/postman/Opal Print.postman_collection.json new file mode 100644 index 00000000..de0a9a56 --- /dev/null +++ b/postman/Opal Print.postman_collection.json @@ -0,0 +1,103 @@ +{ + "info": { + "_postman_id": "47e7c899-66b9-485a-bf81-13002887dbc6", + "name": "Opal Print Service", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "1068308" + }, + "item": [ + { + "name": "generate-pdf", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"xmlData\": \"John Doe123456789501.55\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n}\n" + }, + "url": { + "raw": "http://localhost:4550/api/print/generate-pdf", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "4550", + "path": [ + "api", + "print", + "generate-pdf" + ] + } + }, + "response": [] + }, + { + "name": "enqueue-print-jobs", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"xmlData\": \"This one should fail\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Bob Brown112233445250.00\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Alice Green556677889799.99\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Charlie Black2233445561240.50\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Emma White667788990645.25\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Liam Grey334455667320.75\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Olivia Brown445566778510.65\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Noah Blue223344556985.20\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Ava Red778899001450.10\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"William Green112233445702.95\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Sophia Violet667788990250.50\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"James Yellow334455667890.30\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Isabella Orange445566778395.75\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Benjamin Pink223344556980.00\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Mia Purple778899001215.80\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Lucas Grey112233445467.35\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Amelia White667788990730.15\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Henry Black3344556671005.25\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n },\n {\n \"xmlData\": \"Emily Brown445566778589.45\",\n \"docType\": \"TEST_PDF_definition_id\",\n \"docVersion\": \"test_version_1\"\n }\n]\n\n\n" + }, + "url": { + "raw": "http://localhost:4550/api/print/enqueue-print-jobs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "4550", + "path": [ + "api", + "print", + "enqueue-print-jobs" + ] + } + }, + "response": [] + }, + { + "name": "process-pending-jobs", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"info\": {\n \"general\": {\n \"version\": \"00_1\",\n \"docref\": \"AAA\"\n }\n },\n \"data\": {\n \"job\": {\n \"division\": \"MinimalDivision\",\n \"accountnumber\": \"000001\",\n \"casenumber\": \"CASE-00001\",\n \"dob\": \"1990-01-01\",\n \"defendantname\": \"John Doe\",\n \"sex\": \"Male\",\n \"amountoutstanding\": \"£100.00\",\n \"defendantindefault\": \"No\",\n \"dateproduced\": \"2024-04-09\",\n \"dateoforder\": \"2024-03-01\",\n \"defendantaddress\": {\n \"street\": \"123 Minimal St\",\n \"city\": \"Minimal City\",\n \"postalCode\": \"M1234\"\n },\n \"jobcentreaddress\": {\n \"name\": \"Minimal Job Centre\",\n \"address\": {\n \"street\": \"456 Minimal St\",\n \"city\": \"Job Centre City\",\n \"postalCode\": \"JC123\"\n }\n }\n }\n }\n}\n" + }, + "url": { + "raw": "http://localhost:4550/api/print/process-pending-jobs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "4550", + "path": [ + "api", + "print", + "process-pending-jobs" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/src/main/java/uk/gov/hmcts/opal/config/AsyncConfig.java b/src/main/java/uk/gov/hmcts/opal/config/AsyncConfig.java new file mode 100644 index 00000000..6d539559 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/config/AsyncConfig.java @@ -0,0 +1,10 @@ +package uk.gov.hmcts.opal.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +@Configuration +@EnableAsync +public class AsyncConfig { + +} diff --git a/src/main/java/uk/gov/hmcts/opal/config/DatabaseConfiguration.java b/src/main/java/uk/gov/hmcts/opal/config/DatabaseConfiguration.java index 938c1de5..4cde6dea 100644 --- a/src/main/java/uk/gov/hmcts/opal/config/DatabaseConfiguration.java +++ b/src/main/java/uk/gov/hmcts/opal/config/DatabaseConfiguration.java @@ -35,6 +35,7 @@ public TransactionAwareDataSourceProxy transactionAwareDataSourceProxy( } @Bean + @Primary public PlatformTransactionManager transactionManager( TransactionAwareDataSourceProxy transactionAwareDataSourceProxy ) { diff --git a/src/main/java/uk/gov/hmcts/opal/config/PrintTransactionManagerConfig.java b/src/main/java/uk/gov/hmcts/opal/config/PrintTransactionManagerConfig.java new file mode 100644 index 00000000..7e8fab97 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/config/PrintTransactionManagerConfig.java @@ -0,0 +1,18 @@ +package uk.gov.hmcts.opal.config; + +import jakarta.persistence.EntityManagerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.JpaTransactionManager; + +@Configuration +public class PrintTransactionManagerConfig { + + @Bean(name = "printTransactionManager") + public JpaTransactionManager printTransactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + +} diff --git a/src/main/java/uk/gov/hmcts/opal/controllers/print/PrintRequestController.java b/src/main/java/uk/gov/hmcts/opal/controllers/print/PrintRequestController.java index 292ff580..051eaaa9 100644 --- a/src/main/java/uk/gov/hmcts/opal/controllers/print/PrintRequestController.java +++ b/src/main/java/uk/gov/hmcts/opal/controllers/print/PrintRequestController.java @@ -12,10 +12,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import uk.gov.hmcts.opal.entity.print.PrintJob; +import uk.gov.hmcts.opal.service.print.AsyncPrintJobProcessor; import uk.gov.hmcts.opal.service.print.PrintService; import org.springframework.http.HttpHeaders; import org.springframework.http.ContentDisposition; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -28,6 +30,9 @@ public class PrintRequestController { private final PrintService printService; + private final AsyncPrintJobProcessor asyncPrintJobProcessor; + + @PostMapping(value = "/enqueue-print-jobs", consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Enqueues print jobs for a batch of documents") public ResponseEntity enqueuePrintJobs(@RequestBody List printJobs) { @@ -52,5 +57,15 @@ public ResponseEntity generatePdf(@RequestBody PrintJob printJob) { return ResponseEntity.ok().headers(headers).body(response); } + @PostMapping(value = "/process-pending-jobs") + @Operation(summary = "Processes pending print jobs") + public ResponseEntity processPendingJobs() { + log.info(":POST:processPendingJobs: processing pending print jobs"); + + asyncPrintJobProcessor.processPendingJobsAsync(LocalDateTime.now()); + + return ResponseEntity.ok().body("OK"); + } + } diff --git a/src/main/java/uk/gov/hmcts/opal/entity/print/PrintJob.java b/src/main/java/uk/gov/hmcts/opal/entity/print/PrintJob.java index 9e31123b..06efa0bb 100644 --- a/src/main/java/uk/gov/hmcts/opal/entity/print/PrintJob.java +++ b/src/main/java/uk/gov/hmcts/opal/entity/print/PrintJob.java @@ -7,7 +7,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.Lob; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import lombok.AllArgsConstructor; @@ -41,7 +40,7 @@ public class PrintJob { @Column(name = "job_uuid", nullable = false) private UUID jobId; - @Lob + @Column(name = "xml_data", nullable = false) private String xmlData; diff --git a/src/main/java/uk/gov/hmcts/opal/repository/print/PrintDefinitionRepository.java b/src/main/java/uk/gov/hmcts/opal/repository/print/PrintDefinitionRepository.java index 15188cff..4d40a945 100644 --- a/src/main/java/uk/gov/hmcts/opal/repository/print/PrintDefinitionRepository.java +++ b/src/main/java/uk/gov/hmcts/opal/repository/print/PrintDefinitionRepository.java @@ -1,5 +1,6 @@ package uk.gov.hmcts.opal.repository.print; +import jakarta.transaction.Transactional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import uk.gov.hmcts.opal.entity.print.PrintDefinition; @@ -7,6 +8,6 @@ @Repository public interface PrintDefinitionRepository extends JpaRepository { - + @Transactional PrintDefinition findByDocTypeAndTemplateId(String docType, String templateId); } diff --git a/src/main/java/uk/gov/hmcts/opal/repository/print/PrintJobRepository.java b/src/main/java/uk/gov/hmcts/opal/repository/print/PrintJobRepository.java index 781e715e..6fbf34b7 100644 --- a/src/main/java/uk/gov/hmcts/opal/repository/print/PrintJobRepository.java +++ b/src/main/java/uk/gov/hmcts/opal/repository/print/PrintJobRepository.java @@ -1,8 +1,24 @@ package uk.gov.hmcts.opal.repository.print; +import jakarta.persistence.LockModeType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import uk.gov.hmcts.opal.entity.print.PrintJob; +import uk.gov.hmcts.opal.entity.print.PrintStatus; +import java.time.LocalDateTime; +@Repository public interface PrintJobRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT p FROM PrintJob p WHERE p.status = :status AND p.createdAt <= :cutoffDate") + public Page findPendingJobsForUpdate(@Param("status") PrintStatus status, + @Param("cutoffDate") LocalDateTime cutoffDate, + Pageable pageable); } diff --git a/src/main/java/uk/gov/hmcts/opal/service/print/AsyncPrintJobProcessor.java b/src/main/java/uk/gov/hmcts/opal/service/print/AsyncPrintJobProcessor.java new file mode 100644 index 00000000..43c4d985 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/service/print/AsyncPrintJobProcessor.java @@ -0,0 +1,24 @@ +package uk.gov.hmcts.opal.service.print; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +public class AsyncPrintJobProcessor { + + private final PrintService printService; + + @Autowired + public AsyncPrintJobProcessor(PrintService printService) { + this.printService = printService; + } + + @Async + public void processPendingJobsAsync(LocalDateTime cutoffDate) { + printService.processPendingJobs(cutoffDate); + } + +} diff --git a/src/main/java/uk/gov/hmcts/opal/service/print/PrintService.java b/src/main/java/uk/gov/hmcts/opal/service/print/PrintService.java index 75222d41..872a13b4 100644 --- a/src/main/java/uk/gov/hmcts/opal/service/print/PrintService.java +++ b/src/main/java/uk/gov/hmcts/opal/service/print/PrintService.java @@ -1,18 +1,28 @@ package uk.gov.hmcts.opal.service.print; -import jakarta.transaction.Transactional; + +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import net.sf.saxon.TransformerFactoryImpl; import org.apache.fop.apps.FOUserAgent; import org.apache.fop.apps.Fop; import org.apache.fop.apps.FopFactory; import org.apache.fop.apps.MimeConstants; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import uk.gov.hmcts.opal.entity.print.PrintDefinition; import uk.gov.hmcts.opal.entity.print.PrintJob; import uk.gov.hmcts.opal.entity.print.PrintStatus; import uk.gov.hmcts.opal.repository.print.PrintDefinitionRepository; import uk.gov.hmcts.opal.repository.print.PrintJobRepository; +import uk.gov.hmcts.opal.sftp.SftpLocation; +import uk.gov.hmcts.opal.sftp.SftpOutboundService; import javax.xml.XMLConstants; import javax.xml.transform.Result; @@ -29,7 +39,9 @@ import java.util.UUID; @Service -@Transactional +@Transactional(transactionManager = "printTransactionManager") +@Setter +@Getter @RequiredArgsConstructor @Slf4j(topic = "PrintService") public class PrintService { @@ -40,6 +52,15 @@ public class PrintService { private final PrintJobRepository printJobRepository; + private final SftpOutboundService sftpOutboundService; + + + @Value("${printservice.maxRetries:3}") + private int maxRetries; + + @Value("${printservice.pageSize:100}") + private int pageSize; + public byte[] generatePdf(PrintJob printJob) { // Get print definition from database @@ -53,7 +74,7 @@ public byte[] generatePdf(PrintJob printJob) { FOUserAgent foUserAgent = fopFactory.newFOUserAgent(); Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, outStream); - TransformerFactory factory = TransformerFactory.newInstance(); + TransformerFactory factory = new TransformerFactoryImpl(); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); @@ -61,8 +82,6 @@ public byte[] generatePdf(PrintJob printJob) { // Setup input for XSLT transformation Source src = new StreamSource(xmlReader); - - // Resulting SAX events (the generated FO) must be piped through to FOP Result res = new SAXResult(fop.getDefaultHandler()); // Start XSLT transformation and FOP processing @@ -77,16 +96,17 @@ public byte[] generatePdf(PrintJob printJob) { } - - private PrintDefinition getPrintDefinition(String docType, String templateId) { return printDefinitionRepository.findByDocTypeAndTemplateId(docType, templateId); } + public UUID savePrintJobs(List printJobs) { UUID batchId = UUID.randomUUID(); + log.info("Saving print jobs for batch {}", batchId); + for (PrintJob printJob : printJobs) { printJob.setBatchId(batchId); printJob.setJobId(UUID.randomUUID()); @@ -100,5 +120,77 @@ public UUID savePrintJobs(List printJobs) { } + public void processPendingJobs(LocalDateTime cutoffDate) { + int attempt = 0; + boolean success = false; + + while (attempt < maxRetries && !success) { + try { + attempt++; + processJobsWithLock(cutoffDate); + success = true; + } catch (Exception e) { + if (e instanceof jakarta.persistence.PessimisticLockException) { + log.error("Could not acquire lock, retrying... ({} / {})", attempt, maxRetries); + if (attempt >= maxRetries) { + throw e; // Exceeded max retries, rethrow exception + } + } else { + throw e; // Non-locking exception, rethrow + } + } + } + log.info("Processed pending jobs"); + } + + + protected void processJobsWithLock(LocalDateTime cutoffDate) { + Pageable pageable = PageRequest.of(0, pageSize); + log.info("Page Size: {}", pageSize); + Page page; + do { + page = this.findPendingJobsForUpdate(PrintStatus.PENDING, cutoffDate, pageable); + for (PrintJob job : page.getContent()) { + try { + processJob(job); + } catch (Exception e) { + log.error("Error processing job {}", job.getJobId(), e); + job.setStatus(PrintStatus.FAILED); + printJobRepository.save(job); + } + } + pageable = page.nextPageable(); + } while (page.hasNext()); + } + + + private void processJob(PrintJob job) { + job.setStatus(PrintStatus.IN_PROGRESS); + printJobRepository.save(job); + + byte[] pdfData = generatePdf(job); + + if (pdfData != null) { + savePdfToFile(pdfData, job); + job.setStatus(PrintStatus.COMPLETED); + } else { + job.setStatus(PrintStatus.FAILED); + } + + printJobRepository.save(job); + } + + private void savePdfToFile(byte[] pdfData, PrintJob job) { + String fileName = job.getBatchId() + "_" + job.getJobId() + ".pdf"; + log.info("Saving PDF to file: {}", fileName); + + sftpOutboundService.uploadFile(pdfData, SftpLocation.PRINT_LOCATION.getPath(), fileName); + } + + + private Page findPendingJobsForUpdate(PrintStatus status, LocalDateTime cutoffDate, Pageable pageable) { + log.info("Finding pending jobs for update"); + return printJobRepository.findPendingJobsForUpdate(status, cutoffDate, pageable); + } } diff --git a/src/main/java/uk/gov/hmcts/opal/sftp/SftpLocation.java b/src/main/java/uk/gov/hmcts/opal/sftp/SftpLocation.java index b3397023..cef9ae50 100644 --- a/src/main/java/uk/gov/hmcts/opal/sftp/SftpLocation.java +++ b/src/main/java/uk/gov/hmcts/opal/sftp/SftpLocation.java @@ -33,7 +33,9 @@ public enum SftpLocation { DWP_BAILIFFS_ERROR(INBOUND, "dwp-bailiffs/error", "Error processing for DWP bailiffs"), ALL_PAY(OUTBOUND, "allpay", "Goes to BAIS (pushed)"), - ALL_PAY_ARCHIVE(OUTBOUND, "allpay-archive", "Goes to OAGS (pushed)"); + ALL_PAY_ARCHIVE(OUTBOUND, "allpay-archive", "Goes to OAGS (pushed)"), + + PRINT_LOCATION(OUTBOUND, "print", "Goes to Print Service (pushed)"); private final SftpDirection direction; private final String path; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 62d65d31..83013a0c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -164,3 +164,7 @@ opal: be-developer-config: user-role-permissions: ${BE_DEV_ROLE_PERMISSIONS:} + +printservice: + maxRetries: 3 + pageSize: 100 diff --git a/src/test/java/uk/gov/hmcts/opal/controllers/print/PrintRequestControllerTest.java b/src/test/java/uk/gov/hmcts/opal/controllers/print/PrintRequestControllerTest.java index eda61344..78f3732d 100644 --- a/src/test/java/uk/gov/hmcts/opal/controllers/print/PrintRequestControllerTest.java +++ b/src/test/java/uk/gov/hmcts/opal/controllers/print/PrintRequestControllerTest.java @@ -8,6 +8,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import uk.gov.hmcts.opal.entity.print.PrintJob; +import uk.gov.hmcts.opal.service.print.AsyncPrintJobProcessor; import uk.gov.hmcts.opal.service.print.PrintService; import java.util.Collections; @@ -27,6 +28,9 @@ public class PrintRequestControllerTest { @Mock private PrintService printService; + @Mock + private AsyncPrintJobProcessor asyncPrintJobProcessor; + @InjectMocks private PrintRequestController printRequestController; @@ -65,5 +69,16 @@ void testGeneratePdf() { assertEquals(pdfData, response.getBody()); verify(printService, times(1)).generatePdf(any(PrintJob.class)); } + + @Test + void testProcessPendingJobs() { + // Act + ResponseEntity response = printRequestController.processPendingJobs(); + + // Assert + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("OK", response.getBody()); + verify(asyncPrintJobProcessor, times(1)).processPendingJobsAsync(any()); + } } diff --git a/src/test/java/uk/gov/hmcts/opal/service/print/PrintServiceTest.java b/src/test/java/uk/gov/hmcts/opal/service/print/PrintServiceTest.java index a55024c0..05b1f63b 100644 --- a/src/test/java/uk/gov/hmcts/opal/service/print/PrintServiceTest.java +++ b/src/test/java/uk/gov/hmcts/opal/service/print/PrintServiceTest.java @@ -1,5 +1,6 @@ package uk.gov.hmcts.opal.service.print; + import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.junit.jupiter.api.BeforeEach; @@ -7,29 +8,41 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import uk.gov.hmcts.opal.entity.print.PrintDefinition; import uk.gov.hmcts.opal.entity.print.PrintJob; import uk.gov.hmcts.opal.entity.print.PrintStatus; import uk.gov.hmcts.opal.repository.print.PrintDefinitionRepository; import uk.gov.hmcts.opal.repository.print.PrintJobRepository; +import uk.gov.hmcts.opal.sftp.SftpOutboundService; import java.io.ByteArrayInputStream; +import java.time.LocalDateTime; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -public class PrintServiceTest { +class PrintServiceTest { @Mock private PrintDefinitionRepository printDefinitionRepository; @@ -37,16 +50,28 @@ public class PrintServiceTest { @Mock private PrintJobRepository printJobRepository; + @Mock + private SftpOutboundService sftpOutboundService; + + @InjectMocks private PrintService printService; + + + + + + private PrintJob printJob1; private PrintJob printJob2; private PrintJob printJob; private PrintDefinition printDefinition; + + @BeforeEach - public void setUp() { + void setUp() { // Setup for savePrintJobs test printJob1 = new PrintJob(); printJob1.setXmlData("Data1"); @@ -83,10 +108,13 @@ public void setUp() { + "" + "" + ""); + + + } @Test - public void testSavePrintJobs() { + void testSavePrintJobs() { // Arrange List printJobs = Arrays.asList(printJob1, printJob2); @@ -96,16 +124,17 @@ public void testSavePrintJobs() { // Assert assertEquals(printJob1.getBatchId(), batchId); assertEquals(printJob2.getBatchId(), batchId); - assertEquals(printJob1.getStatus(), PrintStatus.PENDING); - assertEquals(printJob2.getStatus(), PrintStatus.PENDING); + assertEquals(PrintStatus.PENDING, printJob1.getStatus()); + assertEquals(PrintStatus.PENDING, printJob2.getStatus()); verify(printJobRepository, times(2)).save(any(PrintJob.class)); } @Test - public void testGeneratePdf() throws Exception { + void testGeneratePdf() throws Exception { + // Arrange - when(printDefinitionRepository.findByDocTypeAndTemplateId(eq("docType1"), eq("1.0"))) + when(printDefinitionRepository.findByDocTypeAndTemplateId("docType1", "1.0")) .thenReturn(printDefinition); // Act @@ -122,4 +151,70 @@ public void testGeneratePdf() throws Exception { assertTrue(pdfText.contains("Test")); } } + + @Test + void testProcessJobsWithLock() { + // Arrange + when(printDefinitionRepository.findByDocTypeAndTemplateId("docType1", "1.0")) + .thenReturn(printDefinition); + LocalDateTime cutoffDate = LocalDateTime.now(); + Pageable pageable = PageRequest.of(0, 10); + when(printJobRepository.findPendingJobsForUpdate(PrintStatus.PENDING, cutoffDate, pageable)) + .thenReturn(new PageImpl<>(Collections.singletonList(printJob))) + .thenReturn(new PageImpl<>(Collections.emptyList())); + + doNothing().when(sftpOutboundService).uploadFile(any(byte[].class), anyString(), anyString()); + printService.setPageSize(10); + // Act + printService.processJobsWithLock(cutoffDate); + + // Assert + verify(printJobRepository, atLeastOnce()).findPendingJobsForUpdate(eq(PrintStatus.PENDING), eq(cutoffDate), + any(Pageable.class)); + verify(sftpOutboundService, atLeastOnce()).uploadFile(any(byte[].class), anyString(), anyString()); + verify(printJobRepository, atLeastOnce()).save(any(PrintJob.class)); + } + + @Test + void testProcessPendingJobsSuccess() { + // Arrange + PrintService printServiceSpy = Mockito.spy(printService); + printServiceSpy.setMaxRetries(3); + LocalDateTime cutoffDate = LocalDateTime.now(); + doNothing().when(printServiceSpy).processJobsWithLock(cutoffDate); + + // Act + printServiceSpy.processPendingJobs(cutoffDate); + + // Assert + verify(printServiceSpy, times(1)).processJobsWithLock(cutoffDate); + + // Consider verifying the state of printServiceSpy or its dependencies here + // to ensure that processPendingJobs has the expected effects. + } + + @Test + void testProcessPendingJobsMaxRetriesExceeded() { + // Arrange + PrintService printServiceSpy = Mockito.spy(printService); + printServiceSpy.setMaxRetries(3); // Assuming maxRetries is set to 3 + LocalDateTime cutoffDate = LocalDateTime.now(); + + // Simulate failure in processing jobs, causing retries + doThrow(new jakarta.persistence.PessimisticLockException("could not acquire lock")) + .when(printServiceSpy).processJobsWithLock(cutoffDate); + + // Act & Assert + assertThrows(RuntimeException.class, () -> { + printServiceSpy.processPendingJobs(cutoffDate); + }, "Expected processPendingJobs to throw RuntimeException after max retries exceeded"); + + // Verify that processJobsWithLock was attempted maxRetries times + verify(printServiceSpy, times(3)).processJobsWithLock(cutoffDate); + } + } + + + + diff --git a/src/test/java/uk/gov/hmcts/opal/sftp/SftpLocationTest.java b/src/test/java/uk/gov/hmcts/opal/sftp/SftpLocationTest.java index a72b6d8a..d73302c8 100644 --- a/src/test/java/uk/gov/hmcts/opal/sftp/SftpLocationTest.java +++ b/src/test/java/uk/gov/hmcts/opal/sftp/SftpLocationTest.java @@ -17,11 +17,12 @@ class SftpLocationTest { void testGetOutboundLocations() { List outboundLocations = Arrays.asList( SftpLocation.ALL_PAY, - SftpLocation.ALL_PAY_ARCHIVE + SftpLocation.ALL_PAY_ARCHIVE, + SftpLocation.PRINT_LOCATION ); List result = SftpLocation.getOutboundLocations(); - assertEquals(2, outboundLocations.size()); + assertEquals(3, outboundLocations.size()); assertEquals(outboundLocations, result); } @@ -44,7 +45,9 @@ void testGetInboundLocations() { SftpLocation.DWP_BAILIFFS_SUCCESS, SftpLocation.DWP_BAILIFFS_ERROR ); - List outboundLocations = Arrays.asList(SftpLocation.ALL_PAY, SftpLocation.ALL_PAY_ARCHIVE); + List outboundLocations = Arrays.asList(SftpLocation.ALL_PAY, + SftpLocation.ALL_PAY_ARCHIVE, + SftpLocation.PRINT_LOCATION); assertEquals(15, SftpLocation.getInboundLocations().size()); assertEquals(inboundLocations, SftpLocation.getInboundLocations());