diff --git a/build.gradle b/build.gradle index 90c509720..e7b4c3e98 100644 --- a/build.gradle +++ b/build.gradle @@ -473,6 +473,7 @@ spotless { target sourceSets.main.allJava target project(':common').sourceSets.main.allJava target project(':proprietary').sourceSets.main.allJava + target project(':stirling-pdf').sourceSets.main.allJava googleJavaFormat("1.27.0").aosp().reorderImports(false) @@ -574,4 +575,4 @@ tasks.named('build') { doFirst { println "Delegating to :stirling-pdf:bootJar" } -} \ No newline at end of file +} diff --git a/common/src/main/java/stirling/software/common/configuration/AppConfig.java b/common/src/main/java/stirling/software/common/configuration/AppConfig.java index b0d87e617..e799c7c63 100644 --- a/common/src/main/java/stirling/software/common/configuration/AppConfig.java +++ b/common/src/main/java/stirling/software/common/configuration/AppConfig.java @@ -1,7 +1,5 @@ package stirling.software.common.configuration; -import io.github.pixee.security.SystemCommand; -import jakarta.annotation.PostConstruct; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -10,29 +8,22 @@ import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.function.Predicate; - +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; -import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Scope; -import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.thymeleaf.spring6.SpringTemplateEngine; -import org.springframework.core.Ordered; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import stirling.software.common.model.ApplicationProperties; @Lazy @@ -257,7 +248,7 @@ public class AppConfig { return applicationProperties.getSystem().getDatasource(); } - + @Bean(name = "runningProOrHigher") @Profile("default") public boolean runningProOrHigher() { @@ -275,14 +266,14 @@ public class AppConfig { public boolean googleDriveEnabled() { return false; } - + @Bean(name = "license") @Profile("default") public String licenseType() { return "NORMAL"; } - - + + @Bean(name = "disablePixel") public boolean disablePixel() { return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false")); diff --git a/proprietary/src/test/java/stirling/software/proprietary/security/service/TeamServiceTest.java b/proprietary/src/test/java/stirling/software/proprietary/security/service/TeamServiceTest.java new file mode 100644 index 000000000..264c0b59a --- /dev/null +++ b/proprietary/src/test/java/stirling/software/proprietary/security/service/TeamServiceTest.java @@ -0,0 +1,83 @@ +package stirling.software.proprietary.security.service; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.repository.TeamRepository; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TeamServiceTest { + + @Mock + private TeamRepository teamRepository; + + @InjectMocks + private TeamService teamService; + + @Test + void getDefaultTeam() { + var team = new Team(); + team.setName("Marleyans"); + + when(teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME)) + .thenReturn(Optional.of(team)); + + Team result = teamService.getOrCreateDefaultTeam(); + + assertEquals(team, result); + } + + @Test + void createDefaultTeam_whenRepositoryIsEmpty() { + String teamName = "Default"; + var defaultTeam = new Team(); + defaultTeam.setId(1L); + defaultTeam.setName(teamName); + + when(teamRepository.findByName(teamName)) + .thenReturn(Optional.empty()); + when(teamRepository.save(any(Team.class))).thenReturn(defaultTeam); + + Team result = teamService.getOrCreateDefaultTeam(); + + assertEquals(TeamService.DEFAULT_TEAM_NAME, result.getName()); + } + + @Test + void getInternalTeam() { + var team = new Team(); + team.setName("Eldians"); + + when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME)) + .thenReturn(Optional.of(team)); + + Team result = teamService.getOrCreateInternalTeam(); + + assertEquals(team, result); + } + + @Test + void createInternalTeam_whenRepositoryIsEmpty() { + String teamName = "Internal"; + Team internalTeam = new Team(); + internalTeam.setId(2L); + internalTeam.setName(teamName); + + when(teamRepository.findByName(teamName)) + .thenReturn(Optional.empty()); + when(teamRepository.save(any(Team.class))).thenReturn(internalTeam); + when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME)) + .thenReturn(Optional.empty()); + + Team result = teamService.getOrCreateInternalTeam(); + + assertEquals(internalTeam, result); + } +} diff --git a/proprietary/src/test/java/stirling/software/proprietary/security/service/UserServiceTest.java b/proprietary/src/test/java/stirling/software/proprietary/security/service/UserServiceTest.java new file mode 100644 index 000000000..8cb9f5ff5 --- /dev/null +++ b/proprietary/src/test/java/stirling/software/proprietary/security/service/UserServiceTest.java @@ -0,0 +1,317 @@ +package stirling.software.proprietary.security.service; + +import java.sql.SQLException; +import java.util.Locale; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.MessageSource; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.enumeration.Role; +import stirling.software.common.model.exception.UnsupportedProviderException; +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.database.repository.AuthorityRepository; +import stirling.software.proprietary.security.database.repository.UserRepository; +import stirling.software.proprietary.security.model.AuthenticationType; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.security.repository.TeamRepository; +import stirling.software.proprietary.security.session.SessionPersistentRegistry; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private TeamRepository teamRepository; + + @Mock + private AuthorityRepository authorityRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private MessageSource messageSource; + + @Mock + private SessionPersistentRegistry sessionPersistentRegistry; + + @Mock + private DatabaseServiceInterface databaseService; + + @Mock + private ApplicationProperties.Security.OAUTH2 oauth2Properties; + + @InjectMocks + private UserService userService; + + private Team mockTeam; + private User mockUser; + + @BeforeEach + void setUp() { + mockTeam = new Team(); + mockTeam.setId(1L); + mockTeam.setName("Test Team"); + + mockUser = new User(); + mockUser.setId(1L); + mockUser.setUsername("testuser"); + mockUser.setEnabled(true); + } + + @Test + void testSaveUser_WithUsernameAndAuthenticationType_Success() throws Exception { + // Given + String username = "testuser"; + AuthenticationType authType = AuthenticationType.WEB; + + when(teamRepository.findByName("Default")).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + userService.saveUser(username, authType); + + // Then + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithUsernamePasswordAndTeamId_Success() throws Exception { + // Given + String username = "testuser"; + String password = "password123"; + Long teamId = 1L; + String encodedPassword = "encodedPassword123"; + + when(passwordEncoder.encode(password)).thenReturn(encodedPassword); + when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + User result = userService.saveUser(username, password, teamId); + + // Then + assertNotNull(result); + verify(passwordEncoder).encode(password); + verify(teamRepository).findById(teamId); + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithTeamAndRole_Success() throws Exception { + // Given + String username = "testuser"; + String password = "password123"; + String role = Role.ADMIN.getRoleId(); + boolean firstLogin = true; + String encodedPassword = "encodedPassword123"; + + when(passwordEncoder.encode(password)).thenReturn(encodedPassword); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + User result = userService.saveUser(username, password, mockTeam, role, firstLogin); + + // Then + assertNotNull(result); + verify(passwordEncoder).encode(password); + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithInvalidUsername_ThrowsException() throws Exception { + // Given + String invalidUsername = "ab"; // Too short (less than 3 characters) + AuthenticationType authType = AuthenticationType.WEB; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userService.saveUser(invalidUsername, authType) + ); + + verify(userRepository, never()).save(any(User.class)); + verify(databaseService, never()).exportDatabase(); + } + + @Test + void testSaveUser_WithNullPassword_Success() throws Exception { + // Given + String username = "testuser"; + Long teamId = 1L; + + when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + User result = userService.saveUser(username, null, teamId); + + // Then + assertNotNull(result); + verify(passwordEncoder, never()).encode(anyString()); + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithEmptyPassword_Success() throws Exception { + // Given + String username = "testuser"; + String emptyPassword = ""; + Long teamId = 1L; + + when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + User result = userService.saveUser(username, emptyPassword, teamId); + + // Then + assertNotNull(result); + verify(passwordEncoder, never()).encode(anyString()); + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithValidEmail_Success() throws Exception { + // Given + String emailUsername = "test@example.com"; + AuthenticationType authType = AuthenticationType.SSO; + + when(teamRepository.findByName("Default")).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + userService.saveUser(emailUsername, authType); + + // Then + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithReservedUsername_ThrowsException() throws Exception { + // Given + String reservedUsername = "all_users"; + AuthenticationType authType = AuthenticationType.WEB; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userService.saveUser(reservedUsername, authType) + ); + + verify(userRepository, never()).save(any(User.class)); + verify(databaseService, never()).exportDatabase(); + } + + @Test + void testSaveUser_WithAnonymousUser_ThrowsException() throws Exception { + // Given + String anonymousUsername = "anonymoususer"; + AuthenticationType authType = AuthenticationType.WEB; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> userService.saveUser(anonymousUsername, authType) + ); + + verify(userRepository, never()).save(any(User.class)); + verify(databaseService, never()).exportDatabase(); + } + + @Test + void testSaveUser_DatabaseExportThrowsException_StillSavesUser() throws Exception { + // Given + String username = "testuser"; + String password = "password123"; + Long teamId = 1L; + String encodedPassword = "encodedPassword123"; + + when(passwordEncoder.encode(password)).thenReturn(encodedPassword); + when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doThrow(new SQLException("Database export failed")).when(databaseService).exportDatabase(); + + // When & Then + assertThrows(SQLException.class, () -> userService.saveUser(username, password, teamId)); + + // Verify user was still saved before the exception + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithFirstLoginFlag_Success() throws Exception { + // Given + String username = "testuser"; + String password = "password123"; + Long teamId = 1L; + boolean firstLogin = true; + boolean enabled = false; + String encodedPassword = "encodedPassword123"; + + when(passwordEncoder.encode(password)).thenReturn(encodedPassword); + when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + userService.saveUser(username, password, teamId, firstLogin, enabled); + + // Then + verify(passwordEncoder).encode(password); + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + + @Test + void testSaveUser_WithCustomRole_Success() throws Exception { + // Given + String username = "testuser"; + String password = "password123"; + Long teamId = 1L; + String customRole = Role.LIMITED_API_USER.getRoleId(); + String encodedPassword = "encodedPassword123"; + + when(passwordEncoder.encode(password)).thenReturn(encodedPassword); + when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + doNothing().when(databaseService).exportDatabase(); + + // When + userService.saveUser(username, password, teamId, customRole); + + // Then + verify(passwordEncoder).encode(password); + verify(userRepository).save(any(User.class)); + verify(databaseService).exportDatabase(); + } + +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java b/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java index ffc692b70..cd356e8da 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -13,8 +13,6 @@ import java.util.Properties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.EnableScheduling; @@ -210,7 +208,7 @@ public class SPDFApplication { if (arg.startsWith("--spring.profiles.active=")) { String[] provided = arg.substring(arg.indexOf('=') + 1).split(","); if (provided.length > 0) { - log.info("#######0000000000000###############################"); + log.info("#######0000000000000###############################"); return provided; } } @@ -218,14 +216,16 @@ public class SPDFApplication { } log.info("######################################"); // 2. Detect if SecurityConfiguration is present on classpath - if (isClassPresent("stirling.software.proprietary.security.configuration.SecurityConfiguration")) { - log.info("security"); - return new String[] { "security" }; + if (isClassPresent( + "stirling.software.proprietary.security.configuration.SecurityConfiguration")) { + log.info("security"); + return new String[] {"security"}; } else { - log.info("default"); - return new String[] { "default" }; + log.info("default"); + return new String[] {"default"}; } } + private static boolean isClassPresent(String className) { try { Class.forName(className, false, SPDFApplication.class.getClassLoader()); diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index c1a1041f9..cc9daff83 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -80,4 +80,4 @@ public class CleanUrlInterceptor implements HandlerInterceptor { HttpServletResponse response, Object handler, Exception ex) {} -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointInspector.java b/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointInspector.java index 61c89bcab..d9ceb0f9d 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointInspector.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/config/EndpointInspector.java @@ -209,4 +209,4 @@ public class EndpointInspector implements ApplicationListener> extractBookmarks(@RequestParam("file") MultipartFile file) + public List> extractBookmarks(@RequestParam("file") MultipartFile file) throws Exception { PDDocument document = null; try { document = pdfDocumentFactory.load(file); PDDocumentOutline outline = document.getDocumentCatalog().getDocumentOutline(); - + if (outline == null) { log.info("No outline/bookmarks found in PDF"); return new ArrayList<>(); } - + return extractBookmarkItems(document, outline); } finally { if (document != null) { @@ -68,18 +68,19 @@ public class EditTableOfContentsController { } } } - - private List> extractBookmarkItems(PDDocument document, PDDocumentOutline outline) throws Exception { + + private List> extractBookmarkItems( + PDDocument document, PDDocumentOutline outline) throws Exception { List> bookmarks = new ArrayList<>(); PDOutlineItem current = outline.getFirstChild(); - + while (current != null) { Map bookmark = new HashMap<>(); - + // Get bookmark title String title = current.getTitle(); bookmark.put("title", title); - + // Get page number (1-based for UI purposes) PDPage page = current.findDestinationPage(document); if (page != null) { @@ -88,39 +89,40 @@ public class EditTableOfContentsController { } else { bookmark.put("pageNumber", 1); } - + // Process children if any PDOutlineItem child = current.getFirstChild(); if (child != null) { List> children = new ArrayList<>(); PDOutlineNode parent = current; - + while (child != null) { // Recursively process child items Map childBookmark = processChild(document, child); children.add(childBookmark); child = child.getNextSibling(); } - + bookmark.put("children", children); } else { bookmark.put("children", new ArrayList<>()); } - + bookmarks.add(bookmark); current = current.getNextSibling(); } - + return bookmarks; } - - private Map processChild(PDDocument document, PDOutlineItem item) throws Exception { + + private Map processChild(PDDocument document, PDOutlineItem item) + throws Exception { Map bookmark = new HashMap<>(); - + // Get bookmark title String title = item.getTitle(); bookmark.put("title", title); - + // Get page number (1-based for UI purposes) PDPage page = item.findDestinationPage(document); if (page != null) { @@ -129,92 +131,94 @@ public class EditTableOfContentsController { } else { bookmark.put("pageNumber", 1); } - + // Process children if any PDOutlineItem child = item.getFirstChild(); if (child != null) { List> children = new ArrayList<>(); - + while (child != null) { // Recursively process child items Map childBookmark = processChild(document, child); children.add(childBookmark); child = child.getNextSibling(); } - + bookmark.put("children", children); } else { bookmark.put("children", new ArrayList<>()); } - + return bookmark; } - + @PostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data") @Operation( summary = "Edit Table of Contents", description = "Add or edit bookmarks/table of contents in a PDF document.") - public ResponseEntity editTableOfContents(@ModelAttribute EditTableOfContentsRequest request) - throws Exception { + public ResponseEntity editTableOfContents( + @ModelAttribute EditTableOfContentsRequest request) throws Exception { MultipartFile file = request.getFileInput(); PDDocument document = null; try { document = pdfDocumentFactory.load(file); - + // Parse the bookmark data from JSON - List bookmarks = objectMapper.readValue( - request.getBookmarkData(), - new TypeReference>() {}); - + List bookmarks = + objectMapper.readValue( + request.getBookmarkData(), new TypeReference>() {}); + // Create a new document outline PDDocumentOutline outline = new PDDocumentOutline(); document.getDocumentCatalog().setDocumentOutline(outline); - + // Add bookmarks to the outline addBookmarksToOutline(document, outline, bookmarks); - + // Save the document to a byte array ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); - + String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); return WebResponseUtils.bytesToWebResponse( baos.toByteArray(), filename + "_with_toc.pdf", MediaType.APPLICATION_PDF); - + } finally { if (document != null) { document.close(); } } } - - private void addBookmarksToOutline(PDDocument document, PDDocumentOutline outline, List bookmarks) { + + private void addBookmarksToOutline( + PDDocument document, PDDocumentOutline outline, List bookmarks) { for (BookmarkItem bookmark : bookmarks) { PDOutlineItem item = createOutlineItem(document, bookmark); outline.addLast(item); - + if (bookmark.getChildren() != null && !bookmark.getChildren().isEmpty()) { addChildBookmarks(document, item, bookmark.getChildren()); } } } - - private void addChildBookmarks(PDDocument document, PDOutlineItem parent, List children) { + + private void addChildBookmarks( + PDDocument document, PDOutlineItem parent, List children) { for (BookmarkItem child : children) { PDOutlineItem item = createOutlineItem(document, child); parent.addLast(item); - + if (child.getChildren() != null && !child.getChildren().isEmpty()) { addChildBookmarks(document, item, child.getChildren()); } } } - + private PDOutlineItem createOutlineItem(PDDocument document, BookmarkItem bookmark) { PDOutlineItem item = new PDOutlineItem(); item.setTitle(bookmark.getTitle()); - + // Get the target page - adjust for 0-indexed pages in PDFBox int pageIndex = bookmark.getPageNumber() - 1; if (pageIndex < 0) { @@ -222,41 +226,41 @@ public class EditTableOfContentsController { } else if (pageIndex >= document.getNumberOfPages()) { pageIndex = document.getNumberOfPages() - 1; } - + PDPage page = document.getPage(pageIndex); item.setDestination(page); - + return item; } - + // Inner class to represent bookmarks in JSON public static class BookmarkItem { private String title; private int pageNumber; private List children = new ArrayList<>(); - + public String getTitle() { return title; } - + public void setTitle(String title) { this.title = title; } - + public int getPageNumber() { return pageNumber; } - + public void setPageNumber(int pageNumber) { this.pageNumber = pageNumber; } - + public List getChildren() { return children; } - + public void setChildren(List children) { this.children = children; } } -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 5a3261d55..5e37314a6 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -117,9 +117,9 @@ public class MergeController { // Create the document outline PDDocumentOutline outline = new PDDocumentOutline(); mergedDocument.getDocumentCatalog().setDocumentOutline(outline); - + int pageIndex = 0; // Current page index in the merged document - + // Iterate through the original files for (MultipartFile file : files) { // Get the filename without extension to use as bookmark title @@ -128,20 +128,20 @@ public class MergeController { if (title != null && title.contains(".")) { title = title.substring(0, title.lastIndexOf('.')); } - + // Create an outline item for this file PDOutlineItem item = new PDOutlineItem(); item.setTitle(title); - + // Set the destination to the first page of this file in the merged document if (pageIndex < mergedDocument.getNumberOfPages()) { PDPage page = mergedDocument.getPage(pageIndex); item.setDestination(page); } - + // Add the item to the outline outline.addLast(item); - + // Increment page index for the next file try (PDDocument doc = pdfDocumentFactory.load(file)) { pageIndex += doc.getNumberOfPages(); @@ -212,7 +212,7 @@ public class MergeController { } } } - + // Add table of contents if generateToc is true if (generateToc && files.length > 0) { addTableOfContents(mergedDocument, files); @@ -245,4 +245,4 @@ public class MergeController { } } } -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java index b346c0cbe..0e9cd96dc 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java @@ -50,4 +50,4 @@ public class SettingsController { public ResponseEntity> getDisabledEndpoints() { return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses()); } -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index 38fd72c2f..1b65891ac 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -27,9 +27,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; -import stirling.software.SPDF.model.api.PDFWithPageNums; @RestController @RequestMapping("/api/v1/general") diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java index 95f18031a..94cf6aa6d 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java @@ -31,11 +31,11 @@ import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest; import stirling.software.common.model.PdfMetadata; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfMetadataService; import stirling.software.common.util.WebResponseUtils; -import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest; @RestController @RequestMapping("/api/v1/general") diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index b5653eabb..c2bbd31b5 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -31,9 +31,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; -import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; @RestController @RequestMapping("/api/v1/general") diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java index 87ca7537c..32aedf57c 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java @@ -16,8 +16,10 @@ import org.springframework.web.multipart.MultipartFile; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; @@ -39,9 +41,9 @@ public class ConvertEmlToPDF { summary = "Convert EML to PDF", description = "This endpoint converts EML (email) files to PDF format with extensive" - + " customization options. Features include font settings, image constraints, display modes, attachment handling," - + " and HTML debug output. Input: EML file, Output: PDF" - + " or HTML file. Type: SISO") + + " customization options. Features include font settings, image constraints, display modes, attachment handling," + + " and HTML debug output. Input: EML file, Output: PDF" + + " or HTML file. Type: SISO") public ResponseEntity convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) { MultipartFile inputFile = request.getFileInput(); @@ -94,7 +96,8 @@ public class ConvertEmlToPDF { try { byte[] pdfBytes = EmlToPdf.convertEmlToPdf( - runtimePathConfig.getWeasyPrintPath(), // Use configured WeasyPrint path + runtimePathConfig + .getWeasyPrintPath(), // Use configured WeasyPrint path request, fileBytes, originalFilename, @@ -119,12 +122,20 @@ public class ConvertEmlToPDF { .body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8)); } catch (IllegalArgumentException e) { String errorMessage = buildErrorMessage(e, originalFilename); - log.error("EML to PDF conversion failed for {}: {}", originalFilename, errorMessage, e); + log.error( + "EML to PDF conversion failed for {}: {}", + originalFilename, + errorMessage, + e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(errorMessage.getBytes(StandardCharsets.UTF_8)); } catch (RuntimeException e) { String errorMessage = buildErrorMessage(e, originalFilename); - log.error("EML to PDF conversion failed for {}: {}", originalFilename, errorMessage, e); + log.error( + "EML to PDF conversion failed for {}: {}", + originalFilename, + errorMessage, + e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(errorMessage.getBytes(StandardCharsets.UTF_8)); } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java index 780e5b8a5..98f96fbdb 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java @@ -23,9 +23,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import stirling.software.common.model.api.GeneralFile; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.api.GeneralFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.FileToPdf; import stirling.software.common.util.WebResponseUtils; diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index 2c18427eb..8509f5056 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -875,4 +875,4 @@ public class CompressController { } return Math.min(9, currentLevel + 1); } -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index f8cf1c6a0..a3548ed49 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -36,8 +36,8 @@ import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; -import stirling.software.common.service.PostHogService; import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.service.PostHogService; import stirling.software.common.util.FileMonitor; @Service diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index ef5202c70..88d271cfb 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -184,7 +184,8 @@ public class RedactController { String pageNumbersInput = request.getPageNumbers(); String[] parsedPageNumbers = pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0]; - List pageNumbers = GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false); + List pageNumbers = + GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false); Collections.sort(pageNumbers); return pageNumbers; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index 1a4ad08dd..34f8a8daa 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -128,4 +128,4 @@ public class ConverterWebController { model.addAttribute("currentPage", "eml-to-pdf"); return "convert/eml-to-pdf"; } -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 93b55a8e6..72486a28f 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -136,7 +136,7 @@ public class GeneralWebController { model.addAttribute("currentPage", "edit-table-of-contents"); return "edit-table-of-contents"; } - + @GetMapping("/multi-tool") @Hidden public String multiToolForm(Model model) { @@ -350,4 +350,4 @@ public class GeneralWebController { this.type = type; } } -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java index b283e3a6f..2b36f95af 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java @@ -22,8 +22,8 @@ import io.swagger.v3.oas.annotations.Hidden; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import stirling.software.common.model.ApplicationProperties; import stirling.software.SPDF.model.Dependency; +import stirling.software.common.model.ApplicationProperties; @Slf4j @Controller @@ -48,9 +48,7 @@ public class HomeWebController { InputStream is = resource.getInputStream(); String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); - Map> data = - mapper.readValue(json, new TypeReference<>() { - }); + Map> data = mapper.readValue(json, new TypeReference<>() {}); model.addAttribute("dependencies", data.get("dependencies")); } catch (IOException e) { log.error("exception", e); diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/EditTableOfContentsRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/EditTableOfContentsRequest.java index 4fc6c7e97..51e3bc159 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/EditTableOfContentsRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/EditTableOfContentsRequest.java @@ -4,15 +4,21 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; + import stirling.software.common.model.api.PDFFile; @Data @EqualsAndHashCode(callSuper = false) public class EditTableOfContentsRequest extends PDFFile { - - @Schema(description = "Bookmark structure in JSON format", example = "[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2}]}]") + + @Schema( + description = "Bookmark structure in JSON format", + example = + "[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2}]}]") private String bookmarkData; - - @Schema(description = "Whether to replace existing bookmarks or append to them", example = "true") + + @Schema( + description = "Whether to replace existing bookmarks or append to them", + example = "true") private Boolean replaceExisting; -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfByChaptersRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfByChaptersRequest.java index 473dc082d..364faeca4 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfByChaptersRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfByChaptersRequest.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; + import stirling.software.common.model.api.PDFFile; @Data diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java index 89a712fa2..3a89ab686 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; + import stirling.software.common.model.api.PDFFile; @Data diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java index 500bacf6e..149676946 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; + import stirling.software.SPDF.model.api.PDFWithPageNums; @Data @@ -11,31 +12,31 @@ import stirling.software.SPDF.model.api.PDFWithPageNums; public class ConvertToImageRequest extends PDFWithPageNums { @Schema( - description = "The output image format", - defaultValue = "png", - allowableValues = {"png", "jpeg", "jpg", "gif", "webp"}, - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The output image format", + defaultValue = "png", + allowableValues = {"png", "jpeg", "jpg", "gif", "webp"}, + requiredMode = Schema.RequiredMode.REQUIRED) private String imageFormat; @Schema( - description = - "Choose between a single image containing all pages or separate images for each" - + " page", - defaultValue = "multiple", - allowableValues = {"single", "multiple"}, - requiredMode = Schema.RequiredMode.REQUIRED) + description = + "Choose between a single image containing all pages or separate images for each" + + " page", + defaultValue = "multiple", + allowableValues = {"single", "multiple"}, + requiredMode = Schema.RequiredMode.REQUIRED) private String singleOrMultiple; @Schema( - description = "The color type of the output image(s)", - defaultValue = "color", - allowableValues = {"color", "greyscale", "blackwhite"}, - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The color type of the output image(s)", + defaultValue = "color", + allowableValues = {"color", "greyscale", "blackwhite"}, + requiredMode = Schema.RequiredMode.REQUIRED) private String colorType; @Schema( - description = "The DPI (dots per inch) for the output image(s)", - defaultValue = "300", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The DPI (dots per inch) for the output image(s)", + defaultValue = "300", + requiredMode = Schema.RequiredMode.REQUIRED) private Integer dpi; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java index 5f8e72d38..0435e5835 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java @@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFWithPageNums; public class ContainsTextRequest extends PDFWithPageNums { @Schema( - description = "The text to check for", - defaultValue = "text", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The text to check for", + defaultValue = "text", + requiredMode = Schema.RequiredMode.REQUIRED) private String text; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java index a43566b98..a3c57077d 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java @@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFComparison; public class FileSizeRequest extends PDFComparison { @Schema( - description = "Size of the file in bytes", - requiredMode = Schema.RequiredMode.REQUIRED, - defaultValue = "0") + description = "Size of the file in bytes", + requiredMode = Schema.RequiredMode.REQUIRED, + defaultValue = "0") private long fileSize; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java index 3f390d783..05fd10c31 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java @@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFComparison; public class PageRotationRequest extends PDFComparison { @Schema( - description = "Rotation in degrees", - requiredMode = Schema.RequiredMode.REQUIRED, - defaultValue = "0") + description = "Rotation in degrees", + requiredMode = Schema.RequiredMode.REQUIRED, + defaultValue = "0") private int rotation; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java index 8a9ca0ba4..2fa74f040 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java @@ -12,9 +12,9 @@ import stirling.software.SPDF.model.api.PDFComparison; public class PageSizeRequest extends PDFComparison { @Schema( - description = "Standard Page Size", - allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"}, - defaultValue = "A4", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Standard Page Size", + allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"}, + defaultValue = "A4", + requiredMode = Schema.RequiredMode.REQUIRED) private String standardPageSize; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java index 96008efb3..75f75223e 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java @@ -32,11 +32,11 @@ public class MergePdfsRequest extends MultiplePDFFiles { requiredMode = Schema.RequiredMode.REQUIRED, defaultValue = "true") private Boolean removeCertSign; - + @Schema( - description = + description = "Flag indicating whether to generate a table of contents for the merged PDF. If true, a table of contents will be created using the input filenames as chapter names.", requiredMode = Schema.RequiredMode.NOT_REQUIRED, defaultValue = "false") private boolean generateToc = false; -} \ No newline at end of file +} diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java index f2e472c42..f89ba320f 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java @@ -14,33 +14,33 @@ import stirling.software.common.model.api.PDFFile; public class OverlayPdfsRequest extends PDFFile { @Schema( - description = - "An array of PDF files to be used as overlays on the base PDF. The order in" - + " these files is applied based on the selected mode.", - requiredMode = Schema.RequiredMode.REQUIRED) + description = + "An array of PDF files to be used as overlays on the base PDF. The order in" + + " these files is applied based on the selected mode.", + requiredMode = Schema.RequiredMode.REQUIRED) private MultipartFile[] overlayFiles; @Schema( - description = - "The mode of overlaying: 'SequentialOverlay' for sequential application," - + " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'" - + " for fixed repetition based on provided counts", - allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"}, - requiredMode = Schema.RequiredMode.REQUIRED) + description = + "The mode of overlaying: 'SequentialOverlay' for sequential application," + + " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'" + + " for fixed repetition based on provided counts", + allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"}, + requiredMode = Schema.RequiredMode.REQUIRED) private String overlayMode; @Schema( - description = - "An array of integers specifying the number of times each corresponding overlay" - + " file should be applied in the 'FixedRepeatOverlay' mode. This should" - + " match the length of the overlayFiles array.", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = + "An array of integers specifying the number of times each corresponding overlay" + + " file should be applied in the 'FixedRepeatOverlay' mode. This should" + + " match the length of the overlayFiles array.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private int[] counts; @Schema( - description = "Overlay position 0 is Foregound, 1 is Background", - allowableValues = {"0", "1"}, - requiredMode = Schema.RequiredMode.REQUIRED, - type = "number") + description = "Overlay position 0 is Foregound, 1 is Background", + allowableValues = {"0", "1"}, + requiredMode = Schema.RequiredMode.REQUIRED, + type = "number") private int overlayPosition; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/AddStampRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/AddStampRequest.java index 632037a82..48d470a5a 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/AddStampRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/AddStampRequest.java @@ -14,9 +14,9 @@ import stirling.software.SPDF.model.api.PDFWithPageNums; public class AddStampRequest extends PDFWithPageNums { @Schema( - description = "The stamp type (text or image)", - allowableValues = {"text", "image"}, - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The stamp type (text or image)", + allowableValues = {"text", "image"}, + requiredMode = Schema.RequiredMode.REQUIRED) private String stampType; @Schema(description = "The stamp text", defaultValue = "Stirling Software") @@ -26,60 +26,60 @@ public class AddStampRequest extends PDFWithPageNums { private MultipartFile stampImage; @Schema( - description = "The selected alphabet of the stamp text", - allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"}, - defaultValue = "roman") + description = "The selected alphabet of the stamp text", + allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"}, + defaultValue = "roman") private String alphabet = "roman"; @Schema( - description = "The font size of the stamp text and image", - defaultValue = "30", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The font size of the stamp text and image", + defaultValue = "30", + requiredMode = Schema.RequiredMode.REQUIRED) private float fontSize; @Schema( - description = "The rotation of the stamp in degrees", - defaultValue = "0", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The rotation of the stamp in degrees", + defaultValue = "0", + requiredMode = Schema.RequiredMode.REQUIRED) private float rotation; @Schema( - description = "The opacity of the stamp (0.0 - 1.0)", - defaultValue = "0.5", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The opacity of the stamp (0.0 - 1.0)", + defaultValue = "0.5", + requiredMode = Schema.RequiredMode.REQUIRED) private float opacity; @Schema( - description = - "Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center," - + " 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right," - + " 7: top-left, 8: top-center, 9: top-right)", - allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}, - defaultValue = "5", - requiredMode = Schema.RequiredMode.REQUIRED) + description = + "Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center," + + " 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right," + + " 7: top-left, 8: top-center, 9: top-right)", + allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}, + defaultValue = "5", + requiredMode = Schema.RequiredMode.REQUIRED) private int position; @Schema( - description = - "Override X coordinate for stamp placement. If set, it will override the" - + " position-based calculation. Negative value means no override.", - defaultValue = "-1", - requiredMode = Schema.RequiredMode.REQUIRED) + description = + "Override X coordinate for stamp placement. If set, it will override the" + + " position-based calculation. Negative value means no override.", + defaultValue = "-1", + requiredMode = Schema.RequiredMode.REQUIRED) private float overrideX; // Default to -1 indicating no override @Schema( - description = - "Override Y coordinate for stamp placement. If set, it will override the" - + " position-based calculation. Negative value means no override.", - defaultValue = "-1", - requiredMode = Schema.RequiredMode.REQUIRED) + description = + "Override Y coordinate for stamp placement. If set, it will override the" + + " position-based calculation. Negative value means no override.", + defaultValue = "-1", + requiredMode = Schema.RequiredMode.REQUIRED) private float overrideY; // Default to -1 indicating no override @Schema( - description = "Specifies the margin size for the stamp.", - allowableValues = {"small", "medium", "large", "x-large"}, - defaultValue = "medium", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Specifies the margin size for the stamp.", + allowableValues = {"small", "medium", "large", "x-large"}, + defaultValue = "medium", + requiredMode = Schema.RequiredMode.REQUIRED) private String customMargin; @Schema(description = "The color of the stamp text", defaultValue = "#d3d3d3") diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java index 022e92279..63b267196 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java @@ -14,71 +14,71 @@ import stirling.software.common.model.api.PDFFile; public class MetadataRequest extends PDFFile { @Schema( - description = "Delete all metadata if set to true", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Delete all metadata if set to true", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean deleteAll; @Schema( - description = "The author of the document", - defaultValue = "author", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The author of the document", + defaultValue = "author", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String author; @Schema( - description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)", - pattern = "yyyy/MM/dd HH:mm:ss", - defaultValue = "2023/10/01 12:00:00", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)", + pattern = "yyyy/MM/dd HH:mm:ss", + defaultValue = "2023/10/01 12:00:00", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String creationDate; @Schema( - description = "The creator of the document", - defaultValue = "creator", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The creator of the document", + defaultValue = "creator", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String creator; @Schema( - description = "The keywords for the document", - defaultValue = "keywords", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The keywords for the document", + defaultValue = "keywords", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String keywords; @Schema( - description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)", - pattern = "yyyy/MM/dd HH:mm:ss", - defaultValue = "2023/10/01 12:00:00", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)", + pattern = "yyyy/MM/dd HH:mm:ss", + defaultValue = "2023/10/01 12:00:00", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String modificationDate; @Schema( - description = "The producer of the document", - defaultValue = "producer", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The producer of the document", + defaultValue = "producer", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String producer; @Schema( - description = "The subject of the document", - defaultValue = "subject", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The subject of the document", + defaultValue = "subject", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String subject; @Schema( - description = "The title of the document", - defaultValue = "title", - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The title of the document", + defaultValue = "title", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String title; @Schema( - description = "The trapped status of the document", - defaultValue = "False", - allowableValues = {"True", "False", "Unknown"}, - requiredMode = Schema.RequiredMode.NOT_REQUIRED) + description = "The trapped status of the document", + defaultValue = "False", + allowableValues = {"True", "False", "Unknown"}, + requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String trapped; @Schema( - description = - "Map list of key and value of custom parameters. Note these must start with" - + " customKey and customValue if they are non-standard") + description = + "Map list of key and value of custom parameters. Note these must start with" + + " customKey and customValue if they are non-standard") private Map allRequestParams; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/PrintFileRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/PrintFileRequest.java index 18fb19fee..3119c32d7 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/PrintFileRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/misc/PrintFileRequest.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; + import stirling.software.common.model.api.PDFFile; @Data diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java index 3f24389a4..666318a49 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java @@ -12,24 +12,24 @@ import stirling.software.common.model.api.PDFFile; public class AddPasswordRequest extends PDFFile { @Schema( - description = - "The owner password to be added to the PDF file (Restricts what can be done" - + " with the document once it is opened)", - format = "password") + description = + "The owner password to be added to the PDF file (Restricts what can be done" + + " with the document once it is opened)", + format = "password") private String ownerPassword; @Schema( - description = - "The password to be added to the PDF file (Restricts the opening of the" - + " document itself.)", - format = "password") + description = + "The password to be added to the PDF file (Restricts the opening of the" + + " document itself.)", + format = "password") private String password; @Schema( - description = "The length of the encryption key", - allowableValues = {"40", "128", "256"}, - defaultValue = "256", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The length of the encryption key", + allowableValues = {"40", "128", "256"}, + defaultValue = "256", + requiredMode = Schema.RequiredMode.REQUIRED) private int keyLength = 256; @Schema(description = "Whether document assembly is prevented", defaultValue = "false") @@ -39,8 +39,8 @@ public class AddPasswordRequest extends PDFFile { private Boolean preventExtractContent; @Schema( - description = "Whether content extraction for accessibility is prevented", - defaultValue = "false") + description = "Whether content extraction for accessibility is prevented", + defaultValue = "false") private Boolean preventExtractForAccessibility; @Schema(description = "Whether form filling is prevented", defaultValue = "false") @@ -50,8 +50,8 @@ public class AddPasswordRequest extends PDFFile { private Boolean preventModify; @Schema( - description = "Whether modification of annotations is prevented", - defaultValue = "false") + description = "Whether modification of annotations is prevented", + defaultValue = "false") private Boolean preventModifyAnnotations; @Schema(description = "Whether printing of the document is prevented", defaultValue = "false") diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java index 953920c1b..48cb5bc67 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/ManualRedactPdfRequest.java @@ -14,19 +14,19 @@ import stirling.software.common.model.api.security.RedactionArea; @EqualsAndHashCode(callSuper = true) public class ManualRedactPdfRequest extends PDFWithPageNums { @Schema( - description = "A list of areas that should be redacted", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "A list of areas that should be redacted", + requiredMode = Schema.RequiredMode.REQUIRED) private List redactions; @Schema( - description = "Convert the redacted PDF to an image", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Convert the redacted PDF to an image", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean convertPDFToImage; @Schema( - description = "The color used to fully redact certain pages", - defaultValue = "#000000", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The color used to fully redact certain pages", + defaultValue = "#000000", + requiredMode = Schema.RequiredMode.REQUIRED) private String pageRedactionColor; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java index dc13eaa37..279a41a27 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; + import stirling.software.common.model.api.PDFFile; @Data @@ -11,38 +12,38 @@ import stirling.software.common.model.api.PDFFile; public class RedactPdfRequest extends PDFFile { @Schema( - description = "List of text to redact from the PDF", - defaultValue = "text,text2", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "List of text to redact from the PDF", + defaultValue = "text,text2", + requiredMode = Schema.RequiredMode.REQUIRED) private String listOfText; @Schema( - description = "Whether to use regex for the listOfText", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Whether to use regex for the listOfText", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean useRegex; @Schema( - description = "Whether to use whole word search", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Whether to use whole word search", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean wholeWordSearch; @Schema( - description = "The color for redaction", - defaultValue = "#000000", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "The color for redaction", + defaultValue = "#000000", + requiredMode = Schema.RequiredMode.REQUIRED) private String redactColor; @Schema( - description = "Custom padding for redaction", - type = "number", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Custom padding for redaction", + type = "number", + requiredMode = Schema.RequiredMode.REQUIRED) private float customPadding; @Schema( - description = "Convert the redacted PDF to an image", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Convert the redacted PDF to an image", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean convertPDFToImage; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java index 917553894..736fbb20d 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java @@ -12,38 +12,38 @@ import stirling.software.common.model.api.PDFFile; public class SanitizePdfRequest extends PDFFile { @Schema( - description = "Remove JavaScript actions from the PDF", - defaultValue = "true", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Remove JavaScript actions from the PDF", + defaultValue = "true", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean removeJavaScript; @Schema( - description = "Remove embedded files from the PDF", - defaultValue = "true", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Remove embedded files from the PDF", + defaultValue = "true", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean removeEmbeddedFiles; @Schema( - description = "Remove XMP metadata from the PDF", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Remove XMP metadata from the PDF", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean removeXMPMetadata; @Schema( - description = "Remove document info metadata from the PDF", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Remove document info metadata from the PDF", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean removeMetadata; @Schema( - description = "Remove links from the PDF", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Remove links from the PDF", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean removeLinks; @Schema( - description = "Remove fonts from the PDF", - defaultValue = "false", - requiredMode = Schema.RequiredMode.REQUIRED) + description = "Remove fonts from the PDF", + defaultValue = "false", + requiredMode = Schema.RequiredMode.REQUIRED) private Boolean removeFonts; } diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java b/stirling-pdf/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java index a4b95e69a..acd0669c0 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java @@ -14,8 +14,8 @@ import io.micrometer.core.instrument.search.Search; import lombok.RequiredArgsConstructor; -import stirling.software.common.service.PostHogService; import stirling.software.SPDF.config.EndpointInspector; +import stirling.software.common.service.PostHogService; @Service @RequiredArgsConstructor diff --git a/stirling-pdf/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java b/stirling-pdf/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java new file mode 100644 index 000000000..c6a93c931 --- /dev/null +++ b/stirling-pdf/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java @@ -0,0 +1,382 @@ +package stirling.software.SPDF.controller.api; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageTree; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem; +import stirling.software.SPDF.model.api.EditTableOfContentsRequest; +import stirling.software.common.service.CustomPDFDocumentFactory; + +@ExtendWith(MockitoExtension.class) +class EditTableOfContentsControllerTest { + + @Mock + private CustomPDFDocumentFactory pdfDocumentFactory; + + @Mock + private ObjectMapper objectMapper; + + @InjectMocks + private EditTableOfContentsController editTableOfContentsController; + + private MockMultipartFile mockFile; + private PDDocument mockDocument; + private PDDocumentCatalog mockCatalog; + private PDPageTree mockPages; + private PDPage mockPage1; + private PDPage mockPage2; + private PDDocumentOutline mockOutline; + private PDOutlineItem mockOutlineItem; + + @BeforeEach + void setUp() { + mockFile = new MockMultipartFile("file", "test.pdf", "application/pdf", "PDF content".getBytes()); + mockDocument = mock(PDDocument.class); + mockCatalog = mock(PDDocumentCatalog.class); + mockPages = mock(PDPageTree.class); + mockPage1 = mock(PDPage.class); + mockPage2 = mock(PDPage.class); + mockOutline = mock(PDDocumentOutline.class); + mockOutlineItem = mock(PDOutlineItem.class); + } + + @Test + void testExtractBookmarks_WithExistingBookmarks_Success() throws Exception { + // Given + when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument); + when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockCatalog.getDocumentOutline()).thenReturn(mockOutline); + when(mockOutline.getFirstChild()).thenReturn(mockOutlineItem); + + when(mockOutlineItem.getTitle()).thenReturn("Chapter 1"); + when(mockOutlineItem.findDestinationPage(mockDocument)).thenReturn(mockPage1); + when(mockDocument.getPages()).thenReturn(mockPages); + when(mockPages.indexOf(mockPage1)).thenReturn(0); + when(mockOutlineItem.getFirstChild()).thenReturn(null); + when(mockOutlineItem.getNextSibling()).thenReturn(null); + + // When + List> result = editTableOfContentsController.extractBookmarks(mockFile); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + + Map bookmark = result.get(0); + assertEquals("Chapter 1", bookmark.get("title")); + assertEquals(1, bookmark.get("pageNumber")); // 1-based + assertInstanceOf(List.class, bookmark.get("children")); + + verify(mockDocument).close(); + } + + @Test + void testExtractBookmarks_NoOutline_ReturnsEmptyList() throws Exception { + // Given + when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument); + when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockCatalog.getDocumentOutline()).thenReturn(null); + + // When + List> result = editTableOfContentsController.extractBookmarks(mockFile); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(mockDocument).close(); + } + + @Test + void testExtractBookmarks_WithNestedBookmarks_Success() throws Exception { + // Given + PDOutlineItem childItem = mock(PDOutlineItem.class); + + when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument); + when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockCatalog.getDocumentOutline()).thenReturn(mockOutline); + when(mockOutline.getFirstChild()).thenReturn(mockOutlineItem); + + // Parent bookmark + when(mockOutlineItem.getTitle()).thenReturn("Chapter 1"); + when(mockOutlineItem.findDestinationPage(mockDocument)).thenReturn(mockPage1); + when(mockDocument.getPages()).thenReturn(mockPages); + when(mockPages.indexOf(mockPage1)).thenReturn(0); + when(mockOutlineItem.getFirstChild()).thenReturn(childItem); + when(mockOutlineItem.getNextSibling()).thenReturn(null); + + // Child bookmark + when(childItem.getTitle()).thenReturn("Section 1.1"); + when(childItem.findDestinationPage(mockDocument)).thenReturn(mockPage2); + when(mockPages.indexOf(mockPage2)).thenReturn(1); + when(childItem.getFirstChild()).thenReturn(null); + when(childItem.getNextSibling()).thenReturn(null); + + // When + List> result = editTableOfContentsController.extractBookmarks(mockFile); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + + Map parentBookmark = result.get(0); + assertEquals("Chapter 1", parentBookmark.get("title")); + assertEquals(1, parentBookmark.get("pageNumber")); + + @SuppressWarnings("unchecked") + List> children = (List>) parentBookmark.get("children"); + assertEquals(1, children.size()); + + Map childBookmark = children.get(0); + assertEquals("Section 1.1", childBookmark.get("title")); + assertEquals(2, childBookmark.get("pageNumber")); + + verify(mockDocument).close(); + } + + @Test + void testExtractBookmarks_PageNotFound_UsesPageOne() throws Exception { + // Given + when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument); + when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockCatalog.getDocumentOutline()).thenReturn(mockOutline); + when(mockOutline.getFirstChild()).thenReturn(mockOutlineItem); + + when(mockOutlineItem.getTitle()).thenReturn("Chapter 1"); + when(mockOutlineItem.findDestinationPage(mockDocument)).thenReturn(null); // Page not found + when(mockOutlineItem.getFirstChild()).thenReturn(null); + when(mockOutlineItem.getNextSibling()).thenReturn(null); + + // When + List> result = editTableOfContentsController.extractBookmarks(mockFile); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + + Map bookmark = result.get(0); + assertEquals("Chapter 1", bookmark.get("title")); + assertEquals(1, bookmark.get("pageNumber")); // Default to page 1 + + verify(mockDocument).close(); + } + + @Test + void testEditTableOfContents_Success() throws Exception { + // Given + EditTableOfContentsRequest request = new EditTableOfContentsRequest(); + request.setFileInput(mockFile); + request.setBookmarkData("[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[]}]"); + request.setReplaceExisting(true); + + List bookmarks = new ArrayList<>(); + BookmarkItem bookmark = new BookmarkItem(); + bookmark.setTitle("Chapter 1"); + bookmark.setPageNumber(1); + bookmark.setChildren(new ArrayList<>()); + bookmarks.add(bookmark); + + when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument); + when(objectMapper.readValue(eq(request.getBookmarkData()), any(TypeReference.class))).thenReturn(bookmarks); + when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockDocument.getNumberOfPages()).thenReturn(5); + when(mockDocument.getPage(0)).thenReturn(mockPage1); + + // Mock saving behavior + doAnswer(invocation -> { + ByteArrayOutputStream baos = invocation.getArgument(0); + baos.write("mocked pdf content".getBytes()); + return null; + }).when(mockDocument).save(any(ByteArrayOutputStream.class)); + + // When + ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + + // Then + assertNotNull(result); + assertNotNull(result.getBody()); + + ArgumentCaptor outlineCaptor = ArgumentCaptor.forClass(PDDocumentOutline.class); + verify(mockCatalog).setDocumentOutline(outlineCaptor.capture()); + + PDDocumentOutline capturedOutline = outlineCaptor.getValue(); + assertNotNull(capturedOutline); + + verify(mockDocument).close(); + } + + @Test + void testEditTableOfContents_WithNestedBookmarks_Success() throws Exception { + // Given + EditTableOfContentsRequest request = new EditTableOfContentsRequest(); + request.setFileInput(mockFile); + + String bookmarkJson = "[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2,\"children\":[]}]}]"; + request.setBookmarkData(bookmarkJson); + + List bookmarks = new ArrayList<>(); + BookmarkItem parentBookmark = new BookmarkItem(); + parentBookmark.setTitle("Chapter 1"); + parentBookmark.setPageNumber(1); + + BookmarkItem childBookmark = new BookmarkItem(); + childBookmark.setTitle("Section 1.1"); + childBookmark.setPageNumber(2); + childBookmark.setChildren(new ArrayList<>()); + + List children = new ArrayList<>(); + children.add(childBookmark); + parentBookmark.setChildren(children); + bookmarks.add(parentBookmark); + + when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument); + when(objectMapper.readValue(eq(bookmarkJson), any(TypeReference.class))).thenReturn(bookmarks); + when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockDocument.getNumberOfPages()).thenReturn(5); + when(mockDocument.getPage(0)).thenReturn(mockPage1); + when(mockDocument.getPage(1)).thenReturn(mockPage2); + + doAnswer(invocation -> { + ByteArrayOutputStream baos = invocation.getArgument(0); + baos.write("mocked pdf content".getBytes()); + return null; + }).when(mockDocument).save(any(ByteArrayOutputStream.class)); + + // When + ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + + // Then + assertNotNull(result); + verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class)); + verify(mockDocument).close(); + } + + @Test + void testEditTableOfContents_PageNumberBounds_ClampsValues() throws Exception { + // Given + EditTableOfContentsRequest request = new EditTableOfContentsRequest(); + request.setFileInput(mockFile); + request.setBookmarkData("[{\"title\":\"Chapter 1\",\"pageNumber\":-5,\"children\":[]},{\"title\":\"Chapter 2\",\"pageNumber\":100,\"children\":[]}]"); + + List bookmarks = new ArrayList<>(); + + BookmarkItem bookmark1 = new BookmarkItem(); + bookmark1.setTitle("Chapter 1"); + bookmark1.setPageNumber(-5); // Negative page number + bookmark1.setChildren(new ArrayList<>()); + + BookmarkItem bookmark2 = new BookmarkItem(); + bookmark2.setTitle("Chapter 2"); + bookmark2.setPageNumber(100); // Page number exceeds document pages + bookmark2.setChildren(new ArrayList<>()); + + bookmarks.add(bookmark1); + bookmarks.add(bookmark2); + + when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDocument); + when(objectMapper.readValue(eq(request.getBookmarkData()), any(TypeReference.class))).thenReturn(bookmarks); + when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockDocument.getNumberOfPages()).thenReturn(5); + when(mockDocument.getPage(0)).thenReturn(mockPage1); // For negative page number + when(mockDocument.getPage(4)).thenReturn(mockPage2); // For page number exceeding bounds + + doAnswer(invocation -> { + ByteArrayOutputStream baos = invocation.getArgument(0); + baos.write("mocked pdf content".getBytes()); + return null; + }).when(mockDocument).save(any(ByteArrayOutputStream.class)); + + // When + ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + + // Then + assertNotNull(result); + verify(mockDocument).getPage(0); // Clamped to first page + verify(mockDocument).getPage(4); // Clamped to last page + verify(mockDocument).close(); + } + + @Test + void testCreateOutlineItem_ValidPageNumber_Success() throws Exception { + // Given + BookmarkItem bookmark = new BookmarkItem(); + bookmark.setTitle("Test Chapter"); + bookmark.setPageNumber(3); + + when(mockDocument.getNumberOfPages()).thenReturn(5); + when(mockDocument.getPage(2)).thenReturn(mockPage1); // 0-indexed + + // When + Method createOutlineItemMethod = EditTableOfContentsController.class.getDeclaredMethod("createOutlineItem", PDDocument.class, BookmarkItem.class); + createOutlineItemMethod.setAccessible(true); + PDOutlineItem result = (PDOutlineItem) createOutlineItemMethod.invoke(editTableOfContentsController, mockDocument, bookmark); + + // Then + assertNotNull(result); + verify(mockDocument).getPage(2); + } + + + @Test + void testBookmarkItem_GettersAndSetters() { + // Given + BookmarkItem bookmark = new BookmarkItem(); + List children = new ArrayList<>(); + + // When + bookmark.setTitle("Test Title"); + bookmark.setPageNumber(5); + bookmark.setChildren(children); + + // Then + assertEquals("Test Title", bookmark.getTitle()); + assertEquals(5, bookmark.getPageNumber()); + assertEquals(children, bookmark.getChildren()); + } + + @Test + void testEditTableOfContents_IOExceptionDuringLoad_ThrowsException() throws Exception { + // Given + EditTableOfContentsRequest request = new EditTableOfContentsRequest(); + request.setFileInput(mockFile); + + when(pdfDocumentFactory.load(mockFile)).thenThrow(new RuntimeException("Failed to load PDF")); + + // When & Then + assertThrows(RuntimeException.class, () -> editTableOfContentsController.editTableOfContents(request)); + } + + @Test + void testExtractBookmarks_IOExceptionDuringLoad_ThrowsException() throws Exception { + // Given + when(pdfDocumentFactory.load(mockFile)).thenThrow(new RuntimeException("Failed to load PDF")); + + // When & Then + assertThrows(RuntimeException.class, () -> editTableOfContentsController.extractBookmarks(mockFile)); + } +} diff --git a/stirling-pdf/src/test/java/stirling/software/SPDF/controller/api/MergeControllerTest.java b/stirling-pdf/src/test/java/stirling/software/SPDF/controller/api/MergeControllerTest.java new file mode 100644 index 000000000..09f0fd92e --- /dev/null +++ b/stirling-pdf/src/test/java/stirling/software/SPDF/controller/api/MergeControllerTest.java @@ -0,0 +1,279 @@ +package stirling.software.SPDF.controller.api; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageTree; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import stirling.software.common.service.CustomPDFDocumentFactory; + +@ExtendWith(MockitoExtension.class) +class MergeControllerTest { + + @Mock + private CustomPDFDocumentFactory pdfDocumentFactory; + + @InjectMocks + private MergeController mergeController; + + private MockMultipartFile mockFile1; + private MockMultipartFile mockFile2; + private MockMultipartFile mockFile3; + private PDDocument mockDocument; + private PDDocument mockMergedDocument; + private PDDocumentCatalog mockCatalog; + private PDPageTree mockPages; + private PDPage mockPage1; + private PDPage mockPage2; + + @BeforeEach + void setUp() { + mockFile1 = new MockMultipartFile("file1", "document1.pdf", "application/pdf", "PDF content 1".getBytes()); + mockFile2 = new MockMultipartFile("file2", "document2.pdf", "application/pdf", "PDF content 2".getBytes()); + mockFile3 = new MockMultipartFile("file3", "chapter3.pdf", "application/pdf", "PDF content 3".getBytes()); + + mockDocument = mock(PDDocument.class); + mockMergedDocument = mock(PDDocument.class); + mockCatalog = mock(PDDocumentCatalog.class); + mockPages = mock(PDPageTree.class); + mockPage1 = mock(PDPage.class); + mockPage2 = mock(PDPage.class); + } + + @Test + void testAddTableOfContents_WithMultipleFiles_Success() throws Exception { + // Given + MultipartFile[] files = {mockFile1, mockFile2, mockFile3}; + + // Mock the merged document setup + when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockMergedDocument.getNumberOfPages()).thenReturn(6); + when(mockMergedDocument.getPage(0)).thenReturn(mockPage1); + when(mockMergedDocument.getPage(2)).thenReturn(mockPage2); + when(mockMergedDocument.getPage(4)).thenReturn(mockPage1); + + // Mock individual document loading for page count + PDDocument doc1 = mock(PDDocument.class); + PDDocument doc2 = mock(PDDocument.class); + PDDocument doc3 = mock(PDDocument.class); + + when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1); + when(pdfDocumentFactory.load(mockFile2)).thenReturn(doc2); + when(pdfDocumentFactory.load(mockFile3)).thenReturn(doc3); + + when(doc1.getNumberOfPages()).thenReturn(2); + when(doc2.getNumberOfPages()).thenReturn(2); + when(doc3.getNumberOfPages()).thenReturn(2); + + // When + Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class); + addTableOfContentsMethod.setAccessible(true); + addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files); + + // Then + ArgumentCaptor outlineCaptor = ArgumentCaptor.forClass(PDDocumentOutline.class); + verify(mockCatalog).setDocumentOutline(outlineCaptor.capture()); + + PDDocumentOutline capturedOutline = outlineCaptor.getValue(); + assertNotNull(capturedOutline); + + // Verify that documents were loaded for page count + verify(pdfDocumentFactory).load(mockFile1); + verify(pdfDocumentFactory).load(mockFile2); + verify(pdfDocumentFactory).load(mockFile3); + + // Verify document closing + verify(doc1).close(); + verify(doc2).close(); + verify(doc3).close(); + } + + @Test + void testAddTableOfContents_WithSingleFile_Success() throws Exception { + // Given + MultipartFile[] files = {mockFile1}; + + when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockMergedDocument.getNumberOfPages()).thenReturn(3); + when(mockMergedDocument.getPage(0)).thenReturn(mockPage1); + + PDDocument doc1 = mock(PDDocument.class); + when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1); + when(doc1.getNumberOfPages()).thenReturn(3); + + // When + Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class); + addTableOfContentsMethod.setAccessible(true); + addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files); + + // Then + verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class)); + verify(pdfDocumentFactory).load(mockFile1); + verify(doc1).close(); + } + + @Test + void testAddTableOfContents_WithEmptyArray_Success() throws Exception { + // Given + MultipartFile[] files = {}; + when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog); + + // When + Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class); + addTableOfContentsMethod.setAccessible(true); + addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files); + + // Then + verify(mockMergedDocument).getDocumentCatalog(); + verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class)); + verifyNoInteractions(pdfDocumentFactory); + } + + @Test + void testAddTableOfContents_WithIOException_HandlesGracefully() throws Exception { + // Given + MultipartFile[] files = {mockFile1, mockFile2}; + + when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockMergedDocument.getNumberOfPages()).thenReturn(4); + when(mockMergedDocument.getPage(anyInt())).thenReturn(mockPage1); // Use anyInt() to avoid stubbing conflicts + + // First document loads successfully + PDDocument doc1 = mock(PDDocument.class); + when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1); + when(doc1.getNumberOfPages()).thenReturn(2); + + // Second document throws IOException + when(pdfDocumentFactory.load(mockFile2)).thenThrow(new IOException("Failed to load document")); + + // When + Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class); + addTableOfContentsMethod.setAccessible(true); + + // Should not throw exception + assertDoesNotThrow(() -> + addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files) + ); + + // Then + verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class)); + verify(pdfDocumentFactory).load(mockFile1); + verify(pdfDocumentFactory).load(mockFile2); + verify(doc1).close(); + } + + @Test + void testAddTableOfContents_FilenameWithoutExtension_UsesFullName() throws Exception { + // Given + MockMultipartFile fileWithoutExtension = new MockMultipartFile("file", "document_no_ext", "application/pdf", "PDF content".getBytes()); + MultipartFile[] files = {fileWithoutExtension}; + + when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockMergedDocument.getNumberOfPages()).thenReturn(1); + when(mockMergedDocument.getPage(0)).thenReturn(mockPage1); + + PDDocument doc = mock(PDDocument.class); + when(pdfDocumentFactory.load(fileWithoutExtension)).thenReturn(doc); + when(doc.getNumberOfPages()).thenReturn(1); + + // When + Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class); + addTableOfContentsMethod.setAccessible(true); + addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files); + + // Then + verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class)); + verify(doc).close(); + } + + @Test + void testAddTableOfContents_PageIndexExceedsDocumentPages_HandlesGracefully() throws Exception { + // Given + MultipartFile[] files = {mockFile1}; + + when(mockMergedDocument.getDocumentCatalog()).thenReturn(mockCatalog); + when(mockMergedDocument.getNumberOfPages()).thenReturn(0); // No pages in merged document + + PDDocument doc1 = mock(PDDocument.class); + when(pdfDocumentFactory.load(mockFile1)).thenReturn(doc1); + when(doc1.getNumberOfPages()).thenReturn(3); + + // When + Method addTableOfContentsMethod = MergeController.class.getDeclaredMethod("addTableOfContents", PDDocument.class, MultipartFile[].class); + addTableOfContentsMethod.setAccessible(true); + + // Should not throw exception + assertDoesNotThrow(() -> + addTableOfContentsMethod.invoke(mergeController, mockMergedDocument, files) + ); + + // Then + verify(mockCatalog).setDocumentOutline(any(PDDocumentOutline.class)); + verify(mockMergedDocument, never()).getPage(anyInt()); + verify(doc1).close(); + } + + @Test + void testMergeDocuments_Success() throws IOException { + // Given + PDDocument doc1 = mock(PDDocument.class); + PDDocument doc2 = mock(PDDocument.class); + List documents = Arrays.asList(doc1, doc2); + + PDPageTree pages1 = mock(PDPageTree.class); + PDPageTree pages2 = mock(PDPageTree.class); + PDPage page1 = mock(PDPage.class); + PDPage page2 = mock(PDPage.class); + + when(pdfDocumentFactory.createNewDocument()).thenReturn(mockMergedDocument); + when(doc1.getPages()).thenReturn(pages1); + when(doc2.getPages()).thenReturn(pages2); + when(pages1.iterator()).thenReturn(Arrays.asList(page1).iterator()); + when(pages2.iterator()).thenReturn(Arrays.asList(page2).iterator()); + + // When + PDDocument result = mergeController.mergeDocuments(documents); + + // Then + assertNotNull(result); + assertEquals(mockMergedDocument, result); + verify(mockMergedDocument).addPage(page1); + verify(mockMergedDocument).addPage(page2); + } + + @Test + void testMergeDocuments_EmptyList_ReturnsEmptyDocument() throws IOException { + // Given + List documents = Arrays.asList(); + + when(pdfDocumentFactory.createNewDocument()).thenReturn(mockMergedDocument); + + // When + PDDocument result = mergeController.mergeDocuments(documents); + + // Then + assertNotNull(result); + assertEquals(mockMergedDocument, result); + verify(mockMergedDocument, never()).addPage(any(PDPage.class)); + } + +} \ No newline at end of file