added unit tests

This commit is contained in:
Dario Ghunney Ware 2025-06-11 15:18:43 +01:00
parent 1490b29ec0
commit f2468d26c9
44 changed files with 1378 additions and 302 deletions

View File

@ -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)

View File

@ -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

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
@ -218,7 +216,8 @@ public class SPDFApplication {
}
log.info("######################################");
// 2. Detect if SecurityConfiguration is present on classpath
if (isClassPresent("stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
if (isClassPresent(
"stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
log.info("security");
return new String[] {"security"};
} else {
@ -226,6 +225,7 @@ public class SPDFApplication {
return new String[] {"default"};
}
}
private static boolean isClassPresent(String className) {
try {
Class.forName(className, false, SPDFApplication.class.getClassLoader());

View File

@ -25,9 +25,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
// Handler for external static resources
registry.addResourceHandler("/**")
.addResourceLocations(
"file:" + InstallationPathConfig.getStaticPath(),
"classpath:/static/"
);
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
// .setCachePeriod(0); // Optional: disable caching

View File

@ -69,7 +69,8 @@ public class EditTableOfContentsController {
}
}
private List<Map<String, Object>> extractBookmarkItems(PDDocument document, PDDocumentOutline outline) throws Exception {
private List<Map<String, Object>> extractBookmarkItems(
PDDocument document, PDDocumentOutline outline) throws Exception {
List<Map<String, Object>> bookmarks = new ArrayList<>();
PDOutlineItem current = outline.getFirstChild();
@ -114,7 +115,8 @@ public class EditTableOfContentsController {
return bookmarks;
}
private Map<String, Object> processChild(PDDocument document, PDOutlineItem item) throws Exception {
private Map<String, Object> processChild(PDDocument document, PDOutlineItem item)
throws Exception {
Map<String, Object> bookmark = new HashMap<>();
// Get bookmark title
@ -154,8 +156,8 @@ public class EditTableOfContentsController {
@Operation(
summary = "Edit Table of Contents",
description = "Add or edit bookmarks/table of contents in a PDF document.")
public ResponseEntity<byte[]> editTableOfContents(@ModelAttribute EditTableOfContentsRequest request)
throws Exception {
public ResponseEntity<byte[]> editTableOfContents(
@ModelAttribute EditTableOfContentsRequest request) throws Exception {
MultipartFile file = request.getFileInput();
PDDocument document = null;
@ -163,9 +165,9 @@ public class EditTableOfContentsController {
document = pdfDocumentFactory.load(file);
// Parse the bookmark data from JSON
List<BookmarkItem> bookmarks = objectMapper.readValue(
request.getBookmarkData(),
new TypeReference<List<BookmarkItem>>() {});
List<BookmarkItem> bookmarks =
objectMapper.readValue(
request.getBookmarkData(), new TypeReference<List<BookmarkItem>>() {});
// Create a new document outline
PDDocumentOutline outline = new PDDocumentOutline();
@ -189,7 +191,8 @@ public class EditTableOfContentsController {
}
}
private void addBookmarksToOutline(PDDocument document, PDDocumentOutline outline, List<BookmarkItem> bookmarks) {
private void addBookmarksToOutline(
PDDocument document, PDDocumentOutline outline, List<BookmarkItem> bookmarks) {
for (BookmarkItem bookmark : bookmarks) {
PDOutlineItem item = createOutlineItem(document, bookmark);
outline.addLast(item);
@ -200,7 +203,8 @@ public class EditTableOfContentsController {
}
}
private void addChildBookmarks(PDDocument document, PDOutlineItem parent, List<BookmarkItem> children) {
private void addChildBookmarks(
PDDocument document, PDOutlineItem parent, List<BookmarkItem> children) {
for (BookmarkItem child : children) {
PDOutlineItem item = createOutlineItem(document, child);
parent.addLast(item);

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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;
@ -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));
}

View File

@ -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;

View File

@ -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

View File

@ -184,7 +184,8 @@ public class RedactController {
String pageNumbersInput = request.getPageNumbers();
String[] parsedPageNumbers =
pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0];
List<Integer> pageNumbers = GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false);
List<Integer> pageNumbers =
GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false);
Collections.sort(pageNumbers);
return pageNumbers;
}

View File

@ -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<String, List<Dependency>> data =
mapper.readValue(json, new TypeReference<>() {
});
Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {});
model.addAttribute("dependencies", data.get("dependencies"));
} catch (IOException e) {
log.error("exception", e);

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
// Then
assertNotNull(result);
assertEquals(1, result.size());
Map<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
// Then
assertNotNull(result);
assertEquals(1, result.size());
Map<String, Object> parentBookmark = result.get(0);
assertEquals("Chapter 1", parentBookmark.get("title"));
assertEquals(1, parentBookmark.get("pageNumber"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) parentBookmark.get("children");
assertEquals(1, children.size());
Map<String, Object> 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<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
// Then
assertNotNull(result);
assertEquals(1, result.size());
Map<String, Object> 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<BookmarkItem> 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<byte[]> result = editTableOfContentsController.editTableOfContents(request);
// Then
assertNotNull(result);
assertNotNull(result.getBody());
ArgumentCaptor<PDDocumentOutline> 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<BookmarkItem> 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<BookmarkItem> 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<byte[]> 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<BookmarkItem> 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<byte[]> 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<BookmarkItem> 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));
}
}

View File

@ -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<PDDocumentOutline> 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<PDDocument> 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<PDDocument> 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));
}
}