# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: Balázs Szücs <[email protected]>
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: ConnorYoh <[email protected]>
Co-authored-by: Connor Yoh <[email protected]>
Co-authored-by: OUNZAR Aymane <[email protected]>
Co-authored-by: YAOU Reda <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Balázs Szücs <[email protected]>
Co-authored-by: Ludy <[email protected]>
Co-authored-by: tkymmm <[email protected]>
Co-authored-by: Peter Dave Hello <[email protected]>
Co-authored-by: albanobattistella <[email protected]>
Co-authored-by: PingLin8888 <[email protected]>
Co-authored-by: FdaSilvaYY <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: OteJlo <[email protected]>
Co-authored-by: Angel <[email protected]>
Co-authored-by: Ricardo Catarino <[email protected]>
Co-authored-by: Luis Antonio Argüelles González <[email protected]>
Co-authored-by: Dawid Urbański <[email protected]>
Co-authored-by: Stephan Paternotte <[email protected]>
Co-authored-by: Leonardo Santos Paulucio <[email protected]>
Co-authored-by: hamza khalem <[email protected]>
Co-authored-by: IT Creativity + Art Team <[email protected]>
Co-authored-by: Reece Browne <[email protected]>
Co-authored-by: James Brunton <[email protected]>
Co-authored-by: Victor Villarreal <[email protected]>
This commit is contained in:
Anthony Stirling
2025-12-21 10:40:32 +00:00
committed by GitHub
co-authored by ConnorYoh Connor Yoh OUNZAR Aymane YAOU Reda dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> Balázs Szücs Ludy tkymmm Peter Dave Hello albanobattistella PingLin8888 FdaSilvaYY Copilot OteJlo Angel Ricardo Catarino Luis Antonio Argüelles González Dawid Urbański Stephan Paternotte Leonardo Santos Paulucio hamza khalem IT Creativity + Art Team Reece Browne James Brunton Victor Villarreal
parent a5dcdd5bd9
commit 68ed54e398
343 changed files with 25208 additions and 6588 deletions
@@ -0,0 +1,86 @@
package org.apache.pdfbox.examples.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
class ConnectedInputStreamTest {
@Test
void delegates_read_skip_available_mark_reset_and_markSupported() throws IOException {
byte[] data = "hello world".getBytes(StandardCharsets.UTF_8);
ByteArrayInputStream base = new ByteArrayInputStream(data);
HttpURLConnection con = mock(HttpURLConnection.class);
ConnectedInputStream cis = new ConnectedInputStream(con, base);
// mark support
assertTrue(cis.markSupported());
// available at start
assertEquals(data.length, cis.available());
// read single byte
int first = cis.read();
assertEquals('h', first);
// mark here
cis.mark(100);
// read next 4 bytes with read(byte[])
byte[] buf4 = new byte[4];
int n4 = cis.read(buf4);
assertEquals(4, n4);
assertArrayEquals("ello".getBytes(StandardCharsets.UTF_8), buf4);
// read next 1 byte with read(byte[], off, len)
byte[] one = new byte[1];
int n1 = cis.read(one, 0, 1);
assertEquals(1, n1);
assertEquals((int) ' ', one[0] & 0xFF);
// reset to mark and re-read the same 5 bytes ("ello ")
cis.reset();
byte[] again5 = new byte[5];
int n5 = cis.read(again5, 0, 5);
assertEquals(5, n5);
assertArrayEquals("ello ".getBytes(StandardCharsets.UTF_8), again5);
// skip one byte ('w')
long skipped = cis.skip(1);
assertEquals(1, skipped);
// remaining should be "orld" (4 bytes)
assertEquals(4, cis.available());
byte[] rest = new byte[4];
assertEquals(4, cis.read(rest));
assertArrayEquals("orld".getBytes(StandardCharsets.UTF_8), rest);
// end of stream
assertEquals(-1, cis.read());
cis.close();
verify(con).disconnect();
}
@Test
void close_closes_stream_before_disconnect() throws IOException {
InputStream is = mock(InputStream.class);
HttpURLConnection con = mock(HttpURLConnection.class);
ConnectedInputStream cis = new ConnectedInputStream(con, is);
cis.close();
InOrder inOrder = inOrder(is, con);
inOrder.verify(is).close();
inOrder.verify(con).disconnect();
inOrder.verifyNoMoreInteractions();
}
}
@@ -0,0 +1,93 @@
package stirling.software.SPDF.Factories;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.common.model.api.misc.HighContrastColorCombination;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import stirling.software.common.util.misc.ColorSpaceConversionStrategy;
import stirling.software.common.util.misc.CustomColorReplaceStrategy;
import stirling.software.common.util.misc.InvertFullColorStrategy;
import stirling.software.common.util.misc.ReplaceAndInvertColorStrategy;
class ReplaceAndInvertColorFactoryTest {
private ReplaceAndInvertColorFactory factory;
private MultipartFile file;
private EndpointConfiguration endpointConfiguration;
@BeforeEach
void setup() {
endpointConfiguration = mock(EndpointConfiguration.class);
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(true);
factory = new ReplaceAndInvertColorFactory(null, endpointConfiguration);
file = mock(MultipartFile.class);
}
@Test
void whenCustomColor_thenReturnsCustomColorReplaceStrategy() {
ReplaceAndInvert option = ReplaceAndInvert.CUSTOM_COLOR;
HighContrastColorCombination combo = null; // not used for CUSTOM_COLOR
ReplaceAndInvertColorStrategy strategy =
factory.replaceAndInvert(file, option, combo, "#FFFFFF", "#000000");
assertNotNull(strategy);
assertTrue(
strategy instanceof CustomColorReplaceStrategy,
"Expected CustomColorReplaceStrategy for CUSTOM_COLOR");
}
@Test
void whenHighContrastColor_thenReturnsCustomColorReplaceStrategy() {
ReplaceAndInvert option = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
HighContrastColorCombination combo = null;
ReplaceAndInvertColorStrategy strategy =
factory.replaceAndInvert(file, option, combo, "#FFFFFF", "#000000");
assertNotNull(strategy);
assertTrue(
strategy instanceof CustomColorReplaceStrategy,
"Expected CustomColorReplaceStrategy for HIGH_CONTRAST_COLOR");
}
@Test
void whenFullInversion_thenReturnsInvertFullColorStrategy() {
ReplaceAndInvert option = ReplaceAndInvert.FULL_INVERSION;
ReplaceAndInvertColorStrategy strategy =
factory.replaceAndInvert(file, option, null, null, null);
assertNotNull(strategy);
assertTrue(
strategy instanceof InvertFullColorStrategy,
"Expected InvertFullColorStrategy for FULL_INVERSION");
}
@Test
void whenColorSpaceConversion_thenReturnsColorSpaceConversionStrategy() {
ReplaceAndInvert option = ReplaceAndInvert.COLOR_SPACE_CONVERSION;
ReplaceAndInvertColorStrategy strategy =
factory.replaceAndInvert(file, option, null, null, null);
assertNotNull(strategy);
assertTrue(
strategy instanceof ColorSpaceConversionStrategy,
"Expected ColorSpaceConversionStrategy for COLOR_SPACE_CONVERSION");
}
@Test
void whenNullOption_thenReturnsNull() {
ReplaceAndInvertColorStrategy strategy =
factory.replaceAndInvert(file, null, null, null, null);
assertNull(strategy, "Expected null for unsupported/unknown option");
}
}
@@ -1,6 +1,7 @@
package stirling.software.SPDF;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -10,6 +11,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.env.Environment;
import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties;
@ExtendWith(MockitoExtension.class)
@@ -21,6 +23,8 @@ public class SPDFApplicationTest {
@InjectMocks private SPDFApplication sPDFApplication;
@Mock private AppConfig appConfig;
@BeforeEach
public void setUp() {
SPDFApplication.setServerPortStatic("8080");
@@ -36,4 +40,25 @@ public class SPDFApplicationTest {
public void testGetStaticPort() {
assertEquals("8080", SPDFApplication.getStaticPort());
}
@Test
public void testSetServerPortStaticAuto() {
SPDFApplication.setServerPortStatic("auto");
assertEquals("0", SPDFApplication.getStaticPort());
}
@Test
public void testInit() {
when(appConfig.getBackendUrl()).thenReturn("http://localhost");
when(appConfig.getContextPath()).thenReturn("/app");
when(appConfig.getServerPort()).thenReturn("8080");
sPDFApplication.init();
assertEquals("http://localhost", SPDFApplication.getStaticBaseUrl());
assertEquals("/app", SPDFApplication.getStaticContextPath());
assertEquals("8080", SPDFApplication.getStaticPort());
}
// Tests for getActiveProfile removed - method is now private
}
@@ -0,0 +1,185 @@
package stirling.software.SPDF.config;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.common.configuration.RuntimePathConfig;
@ExtendWith(MockitoExtension.class)
class ExternalAppDepConfigTest {
@Mock private EndpointConfiguration endpointConfiguration;
@Mock private RuntimePathConfig runtimePathConfig;
private ExternalAppDepConfig config;
@BeforeEach
void setUp() {
when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/custom/weasyprint");
when(runtimePathConfig.getUnoConvertPath()).thenReturn("/custom/unoconvert");
when(runtimePathConfig.getCalibrePath()).thenReturn("/custom/calibre");
when(runtimePathConfig.getOcrMyPdfPath()).thenReturn("/custom/ocrmypdf");
lenient()
.when(endpointConfiguration.getEndpointsForGroup(anyString()))
.thenReturn(Set.of());
config = new ExternalAppDepConfig(endpointConfiguration, runtimePathConfig);
}
@Test
void commandToGroupMappingIncludesRuntimePaths() throws Exception {
Map<String, List<String>> mapping = getCommandToGroupMapping();
assertEquals(List.of("Weasyprint"), mapping.get("/custom/weasyprint"));
assertEquals(List.of("Unoconvert"), mapping.get("/custom/unoconvert"));
assertEquals(List.of("Calibre"), mapping.get("/custom/calibre"));
assertEquals(List.of("OCRmyPDF"), mapping.get("/custom/ocrmypdf"));
assertEquals(List.of("Ghostscript"), mapping.get("gs"));
}
@Test
void getAffectedFeaturesFormatsEndpoints() throws Exception {
Set<String> endpoints = new LinkedHashSet<>(List.of("pdf-to-html", "img-extract"));
when(endpointConfiguration.getEndpointsForGroup("Ghostscript")).thenReturn(endpoints);
@SuppressWarnings("unchecked")
List<String> features =
(List<String>) invokePrivateMethod(config, "getAffectedFeatures", "Ghostscript");
assertEquals(List.of("PDF To Html", "Image Extract"), features);
}
@Test
void formatEndpointAsFeatureConvertsNames() throws Exception {
String formatted =
(String) invokePrivateMethod(config, "formatEndpointAsFeature", "pdf-img-extract");
assertEquals("PDF Image Extract", formatted);
}
@Test
void capitalizeWordHandlesSpecialCases() throws Exception {
String pdf = (String) invokePrivateMethod(config, "capitalizeWord", "pdf");
String mixed = (String) invokePrivateMethod(config, "capitalizeWord", "tEsT");
String empty = (String) invokePrivateMethod(config, "capitalizeWord", "");
assertEquals("PDF", pdf);
assertEquals("Test", mixed);
assertEquals("", empty);
}
@Test
void isWeasyprintMatchesConfiguredCommands() throws Exception {
boolean directMatch =
(boolean) invokePrivateMethod(config, "isWeasyprint", "/custom/weasyprint");
boolean nameContains =
(boolean) invokePrivateMethod(config, "isWeasyprint", "/usr/bin/weasyprint-cli");
boolean differentCommand = (boolean) invokePrivateMethod(config, "isWeasyprint", "qpdf");
assertTrue(directMatch);
assertTrue(nameContains);
assertFalse(differentCommand);
}
@Test
void versionComparisonHandlesDifferentFormats() {
ExternalAppDepConfig.Version required = new ExternalAppDepConfig.Version("58");
ExternalAppDepConfig.Version installed = new ExternalAppDepConfig.Version("57.9.2");
ExternalAppDepConfig.Version beta = new ExternalAppDepConfig.Version("58.beta");
assertTrue(installed.compareTo(required) < 0);
assertEquals(0, beta.compareTo(required));
assertEquals("58.0.0", beta.toString());
}
@SuppressWarnings("unchecked")
private Map<String, List<String>> getCommandToGroupMapping() throws Exception {
Field field = ExternalAppDepConfig.class.getDeclaredField("commandToGroupMapping");
field.setAccessible(true);
return (Map<String, List<String>>) field.get(config);
}
private Object invokePrivateMethod(Object target, String methodName, Object... args)
throws Exception {
Method method = findMatchingMethod(methodName, args);
method.setAccessible(true);
return method.invoke(target, args);
}
private Method findMatchingMethod(String methodName, Object[] args)
throws NoSuchMethodException {
Method[] methods = ExternalAppDepConfig.class.getDeclaredMethods();
for (Method candidate : methods) {
if (!candidate.getName().equals(methodName)
|| candidate.getParameterCount() != args.length) {
continue;
}
Class<?>[] parameterTypes = candidate.getParameterTypes();
boolean matches = true;
for (int i = 0; i < parameterTypes.length; i++) {
if (!isParameterCompatible(parameterTypes[i], args[i])) {
matches = false;
break;
}
}
if (matches) {
return candidate;
}
}
throw new NoSuchMethodException(
"No matching method found for " + methodName + " with provided arguments");
}
private boolean isParameterCompatible(Class<?> parameterType, Object arg) {
if (arg == null) {
return !parameterType.isPrimitive();
}
Class<?> argumentClass = arg.getClass();
if (parameterType.isPrimitive()) {
return getWrapperType(parameterType).isAssignableFrom(argumentClass);
}
return parameterType.isAssignableFrom(argumentClass);
}
private Class<?> getWrapperType(Class<?> primitiveType) {
if (primitiveType == boolean.class) {
return Boolean.class;
} else if (primitiveType == byte.class) {
return Byte.class;
} else if (primitiveType == short.class) {
return Short.class;
} else if (primitiveType == int.class) {
return Integer.class;
} else if (primitiveType == long.class) {
return Long.class;
} else if (primitiveType == float.class) {
return Float.class;
} else if (primitiveType == double.class) {
return Double.class;
} else if (primitiveType == char.class) {
return Character.class;
}
throw new IllegalArgumentException("Type is not primitive: " + primitiveType);
}
}
@@ -0,0 +1,59 @@
package stirling.software.SPDF.controller.api;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import stirling.software.SPDF.service.LanguageService;
class AdditionalLanguageJsControllerTest {
@Test
void returnsJsWithSupportedLanguagesAndFunction() throws Exception {
LanguageService lang = mock(LanguageService.class);
// LinkedHashSet for deterministic order in the array
when(lang.getSupportedLanguages())
.thenReturn(new LinkedHashSet<>(List.of("de_DE", "en_GB")));
MockMvc mvc =
MockMvcBuilders.standaloneSetup(new AdditionalLanguageJsController(lang)).build();
mvc.perform(get("/js/additionalLanguageCode.js"))
.andExpect(status().isOk())
.andExpect(content().contentType(new MediaType("application", "javascript")))
.andExpect(
content()
.string(
containsString(
"const supportedLanguages ="
+ " [\"de_DE\",\"en_GB\"];")))
.andExpect(content().string(containsString("function getDetailedLanguageCode()")))
.andExpect(content().string(containsString("return \"en_GB\";")));
verify(lang, times(1)).getSupportedLanguages();
}
@Test
void emptySupportedLanguagesYieldsEmptyArray() throws Exception {
LanguageService lang = mock(LanguageService.class);
when(lang.getSupportedLanguages()).thenReturn(Set.of());
MockMvc mvc =
MockMvcBuilders.standaloneSetup(new AdditionalLanguageJsController(lang)).build();
mvc.perform(get("/js/additionalLanguageCode.js"))
.andExpect(status().isOk())
.andExpect(content().contentType(new MediaType("application", "javascript")))
.andExpect(content().string(containsString("const supportedLanguages = [];")));
}
}
@@ -0,0 +1,686 @@
package stirling.software.SPDF.controller.api;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.common.service.CustomPDFDocumentFactory;
@ExtendWith(MockitoExtension.class)
@DisplayName("CropController Tests")
class CropControllerTest {
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@InjectMocks private CropController cropController;
private TestPdfFactory pdfFactory;
@BeforeEach
void setUp() {
pdfFactory = new TestPdfFactory();
}
private static class CropRequestBuilder {
private final CropPdfForm form = new CropPdfForm();
CropRequestBuilder withFile(MockMultipartFile file) {
form.setFileInput(file);
return this;
}
CropRequestBuilder withCoordinates(float x, float y, float width, float height) {
form.setX(x);
form.setY(y);
form.setWidth(width);
form.setHeight(height);
return this;
}
CropRequestBuilder withAutoCrop(boolean autoCrop) {
form.setAutoCrop(autoCrop);
return this;
}
CropRequestBuilder withRemoveDataOutsideCrop(boolean remove) {
form.setRemoveDataOutsideCrop(remove);
return this;
}
CropPdfForm build() {
return form;
}
}
private class TestPdfFactory {
private static final PDType1Font HELVETICA =
new PDType1Font(Standard14Fonts.FontName.HELVETICA);
MockMultipartFile createStandardPdf(String filename) throws IOException {
return createPdf(filename, PDRectangle.LETTER, null);
}
MockMultipartFile createPdfWithContent(String filename, String content) throws IOException {
return createPdf(filename, PDRectangle.LETTER, content);
}
MockMultipartFile createPdfWithSize(String filename, PDRectangle size) throws IOException {
return createPdf(filename, size, null);
}
MockMultipartFile createPdf(String filename, PDRectangle pageSize, String content)
throws IOException {
Path testPdfPath = tempDir.resolve(filename);
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(pageSize);
doc.addPage(page);
if (content != null && !content.isEmpty()) {
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
contentStream.beginText();
contentStream.setFont(HELVETICA, 12);
contentStream.newLineAtOffset(50, pageSize.getHeight() - 50);
contentStream.showText(content);
contentStream.endText();
}
}
doc.save(testPdfPath.toFile());
}
return new MockMultipartFile(
"fileInput",
filename,
MediaType.APPLICATION_PDF_VALUE,
Files.readAllBytes(testPdfPath));
}
MockMultipartFile createPdfWithCenteredContent(String filename, String content)
throws IOException {
Path testPdfPath = tempDir.resolve(filename);
PDRectangle pageSize = PDRectangle.LETTER;
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(pageSize);
doc.addPage(page);
if (content != null && !content.isEmpty()) {
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
contentStream.beginText();
contentStream.setFont(HELVETICA, 12);
float x = pageSize.getWidth() / 2 - 50;
float y = pageSize.getHeight() / 2;
contentStream.newLineAtOffset(x, y);
contentStream.showText(content);
contentStream.endText();
}
}
doc.save(testPdfPath.toFile());
}
return new MockMultipartFile(
"fileInput",
filename,
MediaType.APPLICATION_PDF_VALUE,
Files.readAllBytes(testPdfPath));
}
}
@Nested
@DisplayName("Manual Crop with PDFBox")
class ManualCropPDFBoxTests {
@Test
@DisplayName(
"Should successfully crop PDF using PDFBox when removeDataOutsideCrop is false")
void shouldCropPdfSuccessfullyWithPDFBox() throws IOException {
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
CropPdfForm request =
new CropRequestBuilder()
.withFile(testFile)
.withCoordinates(50f, 50f, 512f, 692f)
.withRemoveDataOutsideCrop(false)
.withAutoCrop(false)
.build();
PDDocument mockDocument = mock(PDDocument.class);
PDDocument newDocument = mock(PDDocument.class);
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
assertThat(response)
.isNotNull()
.extracting(ResponseEntity::getStatusCode, ResponseEntity::getBody)
.satisfies(
tuple -> {
assertThat(tuple.get(0)).isEqualTo(HttpStatus.OK);
assertThat(tuple.get(1)).isNotNull();
});
verify(pdfDocumentFactory).load(request);
verify(pdfDocumentFactory).createNewDocumentBasedOnOldDocument(mockDocument);
verify(mockDocument, times(1)).close();
verify(newDocument, times(1)).close();
}
@ParameterizedTest
@CsvSource({"50, 50, 512, 692", "0, 0, 300, 400", "100, 100, 400, 600"})
@DisplayName("Should handle various coordinate sets correctly")
void shouldHandleVariousCoordinates(float x, float y, float width, float height)
throws IOException {
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
CropPdfForm request =
new CropRequestBuilder()
.withFile(testFile)
.withCoordinates(x, y, width, height)
.withRemoveDataOutsideCrop(false)
.withAutoCrop(false)
.build();
PDDocument mockDocument = mock(PDDocument.class);
PDDocument newDocument = mock(PDDocument.class);
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
verify(pdfDocumentFactory).load(request);
verify(mockDocument, times(1)).close();
verify(newDocument, times(1)).close();
}
}
@Nested
@DisplayName("Auto Crop Functionality")
@Tag("integration")
class AutoCropTests {
private TestPdfFactory autoCropPdfFactory;
@BeforeEach
void setUp() {
autoCropPdfFactory = new TestPdfFactory();
}
@Test
@DisplayName("Should auto-crop PDF with content successfully")
void shouldAutoCropPdfSuccessfully() throws IOException {
MockMultipartFile testFile =
autoCropPdfFactory.createPdfWithCenteredContent(
"test_autocrop.pdf", "Test Content for Auto Crop");
CropPdfForm request =
new CropRequestBuilder().withFile(testFile).withAutoCrop(true).build();
// Mock the pdfDocumentFactory to load real PDFs
try (PDDocument sourceDoc = Loader.loadPDF(testFile.getBytes());
PDDocument newDoc = new PDDocument()) {
when(pdfDocumentFactory.load(request)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc))
.thenReturn(newDoc);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
try (PDDocument result = Loader.loadPDF(response.getBody())) {
assertThat(result.getNumberOfPages()).isEqualTo(1);
PDPage page = result.getPage(0);
assertThat(page).isNotNull();
assertThat(page.getMediaBox()).isNotNull();
}
}
}
@Test
@DisplayName("Should handle PDF with minimal content")
void shouldHandleMinimalContentPdf() throws IOException {
MockMultipartFile testFile =
autoCropPdfFactory.createPdfWithContent("minimal.pdf", "X");
CropPdfForm request =
new CropRequestBuilder().withFile(testFile).withAutoCrop(true).build();
// Mock the pdfDocumentFactory to load real PDFs
try (PDDocument sourceDoc = Loader.loadPDF(testFile.getBytes());
PDDocument newDoc = new PDDocument()) {
when(pdfDocumentFactory.load(request)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc))
.thenReturn(newDoc);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
Assertions.assertNotNull(response.getBody());
try (PDDocument result = Loader.loadPDF(response.getBody())) {
assertThat(result.getNumberOfPages()).isEqualTo(1);
}
}
}
}
@Nested
@DisplayName("Content Bounds Detection")
class ContentBoundsDetectionTests {
private Method detectContentBoundsMethod;
private static BufferedImage createWhiteImage(int width, int height) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, 0xFFFFFF);
}
}
return image;
}
private static BufferedImage createImageFilledWith(int width, int height, int color) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, color);
}
}
return image;
}
private static void drawBlackRectangle(
BufferedImage image, int x1, int y1, int x2, int y2) {
for (int x = x1; x < x2; x++) {
for (int y = y1; y < y2; y++) {
image.setRGB(x, y, 0x000000);
}
}
}
private static void drawDarkerRectangle(
BufferedImage image, int x1, int y1, int x2, int y2, int color) {
for (int x = x1; x < x2; x++) {
for (int y = y1; y < y2; y++) {
image.setRGB(x, y, color);
}
}
}
@BeforeEach
void setUp() throws NoSuchMethodException {
detectContentBoundsMethod =
CropController.class.getDeclaredMethod(
"detectContentBounds", BufferedImage.class);
detectContentBoundsMethod.setAccessible(true);
}
@Test
@DisplayName("Should detect full image bounds for all white image")
void shouldDetectFullBoundsForWhiteImage() throws Exception {
BufferedImage whiteImage = createWhiteImage(100, 100);
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, whiteImage);
assertThat(bounds).containsExactly(0, 0, 99, 99);
}
@Test
@DisplayName("Should detect black rectangle bounds correctly")
void shouldDetectBlackRectangleBounds() throws Exception {
BufferedImage image = createWhiteImage(100, 100);
drawBlackRectangle(image, 25, 25, 75, 75);
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
assertThat(bounds).containsExactly(25, 25, 74, 74);
}
@Test
@DisplayName("Should detect content at image edges")
void shouldDetectContentAtEdges() throws Exception {
BufferedImage image = createWhiteImage(100, 100);
image.setRGB(0, 0, 0x000000);
image.setRGB(99, 0, 0x000000);
image.setRGB(0, 99, 0x000000);
image.setRGB(99, 99, 0x000000);
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
assertThat(bounds).containsExactly(0, 0, 99, 99);
}
@Test
@DisplayName("Should include noise pixels in bounds detection")
void shouldIncludeNoiseInBounds() throws Exception {
BufferedImage image = createWhiteImage(100, 100);
image.setRGB(10, 10, 0xF0F0F0);
image.setRGB(90, 90, 0xF0F0F0);
drawBlackRectangle(image, 30, 30, 70, 70);
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
assertThat(bounds).containsExactly(10, 9, 90, 89);
}
@Test
@DisplayName("Should treat gray pixels below threshold as content")
void shouldTreatGrayPixelsAsContent() throws Exception {
BufferedImage image = createImageFilledWith(50, 50, 0xF0F0F0);
drawDarkerRectangle(image, 20, 20, 30, 30, 0xC0C0C0);
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
assertThat(bounds).containsExactly(0, 0, 49, 49);
}
}
@Nested
@DisplayName("White Pixel Detection")
class WhitePixelDetectionTests {
private Method isWhiteMethod;
@BeforeEach
void setUp() throws NoSuchMethodException {
isWhiteMethod = CropController.class.getDeclaredMethod("isWhite", int.class, int.class);
isWhiteMethod.setAccessible(true);
}
@Test
@DisplayName("Should identify pure white pixels")
void shouldIdentifyWhitePixels() throws Exception {
assertThat((Boolean) isWhiteMethod.invoke(null, 0xFFFFFFFF, 250)).isTrue();
assertThat((Boolean) isWhiteMethod.invoke(null, 0xFFF0F0F0, 240)).isTrue();
}
@Test
@DisplayName("Should identify black pixels as non-white")
void shouldIdentifyBlackPixels() throws Exception {
assertThat((Boolean) isWhiteMethod.invoke(null, 0xFF000000, 250)).isFalse();
assertThat((Boolean) isWhiteMethod.invoke(null, 0xFF101010, 250)).isFalse();
}
@ParameterizedTest
@ValueSource(ints = {0xFFFFFFFF, 0xFFFAFAFA, 0xFFF5F5F5})
@DisplayName("Should identify various white shades")
void shouldIdentifyVariousWhiteShades(int pixelColor) throws Exception {
assertThat((Boolean) isWhiteMethod.invoke(null, pixelColor, 240)).isTrue();
}
@ParameterizedTest
@ValueSource(ints = {0xFF000000, 0xFF101010, 0xFF808080})
@DisplayName("Should identify various non-white shades")
void shouldIdentifyNonWhiteShades(int pixelColor) throws Exception {
assertThat((Boolean) isWhiteMethod.invoke(null, pixelColor, 250)).isFalse();
}
}
@Nested
@DisplayName("CropBounds Conversion")
class CropBoundsTests {
private Class<?> cropBoundsClass;
private Method fromPixelsMethod;
@BeforeEach
void setUp() throws ClassNotFoundException, NoSuchMethodException {
cropBoundsClass =
Class.forName(
"stirling.software.SPDF.controller.api.CropController$CropBounds");
fromPixelsMethod =
cropBoundsClass.getDeclaredMethod(
"fromPixels", int[].class, float.class, float.class);
fromPixelsMethod.setAccessible(true);
}
@Test
@DisplayName("Should convert pixel bounds to PDF coordinates correctly")
void shouldConvertPixelBoundsToPdfCoordinates() throws Exception {
int[] pixelBounds = {10, 20, 110, 120};
float scaleX = 0.5f;
float scaleY = 0.5f;
Object bounds = fromPixelsMethod.invoke(null, pixelBounds, scaleX, scaleY);
assertThat(getFloatField(bounds, "x")).isCloseTo(5.0f, within(0.01f));
assertThat(getFloatField(bounds, "y")).isCloseTo(10.0f, within(0.01f));
assertThat(getFloatField(bounds, "width")).isCloseTo(50.0f, within(0.01f));
assertThat(getFloatField(bounds, "height")).isCloseTo(50.0f, within(0.01f));
}
@ParameterizedTest
@CsvSource({
"0, 0, 100, 100, 1.0, 1.0",
"10, 20, 50, 80, 2.0, 2.0",
"5, 5, 25, 25, 0.5, 0.5"
})
@DisplayName("Should handle various scale factors")
void shouldHandleVariousScaleFactors(
int x1, int y1, int x2, int y2, float scaleX, float scaleY) throws Exception {
int[] pixelBounds = {x1, y1, x2, y2};
Object bounds = fromPixelsMethod.invoke(null, pixelBounds, scaleX, scaleY);
assertThat(bounds).isNotNull();
assertThat(getFloatField(bounds, "width")).isGreaterThan(0);
assertThat(getFloatField(bounds, "height")).isGreaterThan(0);
}
@Test
@DisplayName("Should throw exception for invalid pixel bounds array")
void shouldThrowExceptionForInvalidArray() {
int[] invalidBounds = {10, 20, 30};
assertThatThrownBy(() -> fromPixelsMethod.invoke(null, invalidBounds, 1.0f, 1.0f))
.isInstanceOf(Exception.class)
.hasCauseInstanceOf(IllegalArgumentException.class)
.cause()
.hasMessageContaining("pixelBounds array must contain exactly 4 elements");
}
private float getFloatField(Object obj, String fieldName) throws Exception {
Method getter = cropBoundsClass.getDeclaredMethod(fieldName);
return (Float) getter.invoke(obj);
}
}
@Nested
@DisplayName("Error Handling")
class ErrorHandlingTests {
@Test
@DisplayName("Should throw exception for corrupt PDF file")
void shouldThrowExceptionForCorruptPdf() throws IOException {
MockMultipartFile corruptFile =
new MockMultipartFile(
"fileInput",
"corrupt.pdf",
MediaType.APPLICATION_PDF_VALUE,
"not a valid pdf content".getBytes());
CropPdfForm request =
new CropRequestBuilder()
.withFile(corruptFile)
.withCoordinates(50f, 50f, 512f, 692f)
.withRemoveDataOutsideCrop(false)
.withAutoCrop(false)
.build();
when(pdfDocumentFactory.load(request)).thenThrow(new IOException("Invalid PDF format"));
assertThatThrownBy(() -> cropController.cropPdf(request))
.isInstanceOf(IOException.class)
.hasMessageContaining("Invalid PDF format");
verify(pdfDocumentFactory).load(request);
}
@Test
@DisplayName("Should throw exception when coordinates are missing for manual crop")
void shouldThrowExceptionForMissingCoordinates() throws IOException {
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
CropPdfForm request =
new CropRequestBuilder().withFile(testFile).withAutoCrop(false).build();
assertThatThrownBy(() -> cropController.cropPdf(request))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Crop coordinates (x, y, width, height) are required when auto-crop is not enabled");
}
@Test
@DisplayName("Should handle negative coordinates gracefully")
void shouldHandleNegativeCoordinates() throws IOException {
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
CropPdfForm request =
new CropRequestBuilder()
.withFile(testFile)
.withCoordinates(-10f, 50f, 512f, 692f)
.withRemoveDataOutsideCrop(false)
.withAutoCrop(false)
.build();
PDDocument mockDocument = mock(PDDocument.class);
PDDocument newDocument = mock(PDDocument.class);
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
assertThatCode(() -> cropController.cropPdf(request)).doesNotThrowAnyException();
verify(mockDocument, times(1)).close();
verify(newDocument, times(1)).close();
}
@Test
@DisplayName("Should handle zero width or height")
void shouldHandleZeroDimensions() throws IOException {
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
CropPdfForm request =
new CropRequestBuilder()
.withFile(testFile)
.withCoordinates(50f, 50f, 0f, 692f)
.withRemoveDataOutsideCrop(false)
.withAutoCrop(false)
.build();
PDDocument mockDocument = mock(PDDocument.class);
PDDocument newDocument = mock(PDDocument.class);
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
assertThatCode(() -> cropController.cropPdf(request)).doesNotThrowAnyException();
verify(mockDocument, times(1)).close();
verify(newDocument, times(1)).close();
}
}
@Nested
@DisplayName("PDF Content Verification")
@Tag("integration")
class PdfContentVerificationTests {
private static PDRectangle getPageSize(String name) {
return switch (name) {
case "LETTER" -> PDRectangle.LETTER;
case "A4" -> PDRectangle.A4;
case "LEGAL" -> PDRectangle.LEGAL;
default -> PDRectangle.LETTER;
};
}
@Test
@DisplayName("Should produce PDF with correct dimensions after crop")
void shouldProducePdfWithCorrectDimensions() throws IOException {
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
float expectedWidth = 400f;
float expectedHeight = 500f;
CropPdfForm request =
new CropRequestBuilder()
.withFile(testFile)
.withCoordinates(50f, 50f, expectedWidth, expectedHeight)
.withRemoveDataOutsideCrop(false)
.withAutoCrop(false)
.build();
PDDocument mockDocument = mock(PDDocument.class);
PDDocument newDocument = mock(PDDocument.class);
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@ParameterizedTest
@CsvSource({"test1.pdf, LETTER", "test2.pdf, A4", "test3.pdf, LEGAL"})
@DisplayName("Should handle different page sizes")
void shouldHandleDifferentPageSizes(String filename, String pageSizeName)
throws IOException {
PDRectangle pageSize = getPageSize(pageSizeName);
MockMultipartFile testFile = pdfFactory.createPdfWithSize(filename, pageSize);
CropPdfForm request =
new CropRequestBuilder()
.withFile(testFile)
.withCoordinates(50f, 50f, 300f, 400f)
.withRemoveDataOutsideCrop(false)
.withAutoCrop(false)
.build();
PDDocument mockDocument = mock(PDDocument.class);
PDDocument newDocument = mock(PDDocument.class);
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
verify(mockDocument, times(1)).close();
verify(newDocument, times(1)).close();
}
}
}
@@ -1,7 +1,6 @@
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;
@@ -24,7 +23,6 @@ import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
@@ -87,13 +85,9 @@ class EditTableOfContentsControllerTest {
when(mockOutlineItem.getNextSibling()).thenReturn(null);
// When
ResponseEntity<List<Map<String, Object>>> response =
editTableOfContentsController.extractBookmarks(mockFile);
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
// Then
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
List<Map<String, Object>> result = response.getBody();
assertNotNull(result);
assertEquals(1, result.size());
@@ -113,13 +107,9 @@ class EditTableOfContentsControllerTest {
when(mockCatalog.getDocumentOutline()).thenReturn(null);
// When
ResponseEntity<List<Map<String, Object>>> response =
editTableOfContentsController.extractBookmarks(mockFile);
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
// Then
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
List<Map<String, Object>> result = response.getBody();
assertNotNull(result);
assertTrue(result.isEmpty());
verify(mockDocument).close();
@@ -151,13 +141,9 @@ class EditTableOfContentsControllerTest {
when(childItem.getNextSibling()).thenReturn(null);
// When
ResponseEntity<List<Map<String, Object>>> response =
editTableOfContentsController.extractBookmarks(mockFile);
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
// Then
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
List<Map<String, Object>> result = response.getBody();
assertNotNull(result);
assertEquals(1, result.size());
@@ -191,13 +177,9 @@ class EditTableOfContentsControllerTest {
when(mockOutlineItem.getNextSibling()).thenReturn(null);
// When
ResponseEntity<List<Map<String, Object>>> response =
editTableOfContentsController.extractBookmarks(mockFile);
List<Map<String, Object>> result = editTableOfContentsController.extractBookmarks(mockFile);
// Then
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
List<Map<String, Object>> result = response.getBody();
assertNotNull(result);
assertEquals(1, result.size());
@@ -1,8 +1,6 @@
package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*;
import java.io.IOException;
@@ -39,10 +37,8 @@ class MergeControllerTest {
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;
@@ -67,10 +63,10 @@ class MergeControllerTest {
MediaType.APPLICATION_PDF_VALUE,
"PDF content 3".getBytes());
mockDocument = mock(PDDocument.class);
PDDocument mockDocument = mock(PDDocument.class);
mockMergedDocument = mock(PDDocument.class);
mockCatalog = mock(PDDocumentCatalog.class);
mockPages = mock(PDPageTree.class);
PDPageTree mockPages = mock(PDPageTree.class);
mockPage1 = mock(PDPage.class);
mockPage2 = mock(PDPage.class);
}
@@ -1,20 +1,21 @@
package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.common.service.CustomPDFDocumentFactory;
@ExtendWith({MockitoExtension.class})
class RearrangePagesPDFControllerTest {
@Mock private CustomPDFDocumentFactory mockPdfDocumentFactory;
@@ -23,7 +24,6 @@ class RearrangePagesPDFControllerTest {
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
sut = new RearrangePagesPDFController(mockPdfDocumentFactory);
}
@@ -1,8 +1,6 @@
package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -61,7 +59,7 @@ public class RotationControllerTest {
}
@Test
public void testRotatePDFInvalidAngle() throws IOException {
public void testRotatePDFInvalidAngle() {
// Create a mock file
MockMultipartFile mockFile =
new MockMultipartFile(
@@ -0,0 +1,266 @@
package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.apache.pdfbox.pdmodel.PDDocument;
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.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.ProcessExecutor.Processes;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertEbookToPDFControllerTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@Mock private EndpointConfiguration endpointConfiguration;
@InjectMocks private ConvertEbookToPDFController controller;
@Test
void convertEbookToPdf_buildsCalibreCommandAndCleansUp() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile ebookFile =
new MockMultipartFile(
"fileInput", "ebook.epub", "application/epub+zip", "content".getBytes());
ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest();
request.setFileInput(ebookFile);
request.setEmbedAllFonts(true);
request.setIncludeTableOfContents(true);
request.setIncludePageNumbers(true);
Path workingDir = Files.createTempDirectory("ebook-convert-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
AtomicReference<Path> deletedDir = new AtomicReference<>();
Mockito.doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
deletedDir.set(dir);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
PDDocument mockDocument = Mockito.mock(PDDocument.class);
when(pdfDocumentFactory.load(any(File.class))).thenReturn(mockDocument);
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
ProcessExecutor executor = Mockito.mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = Mockito.mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
Path expectedInput = workingDir.resolve("ebook.epub");
Path expectedOutput = workingDir.resolve("ebook.pdf");
when(executor.runCommandWithOutputHandling(
commandCaptor.capture(), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "pdf");
return execResult;
});
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("result".getBytes());
wr.when(
() ->
WebResponseUtils.pdfDocToWebResponse(
mockDocument, "ebook_convertedToPDF.pdf"))
.thenReturn(expectedResponse);
gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf"))
.thenReturn("ebook_convertedToPDF.pdf");
ResponseEntity<byte[]> response = controller.convertEbookToPdf(request);
assertSame(expectedResponse, response);
List<String> command = commandCaptor.getValue();
assertEquals(6, command.size());
assertEquals("ebook-convert", command.get(0));
assertEquals(expectedInput.toString(), command.get(1));
assertEquals(expectedOutput.toString(), command.get(2));
assertEquals("--embed-all-fonts", command.get(3));
assertEquals("--pdf-add-toc", command.get(4));
assertEquals("--pdf-page-numbers", command.get(5));
assertFalse(Files.exists(expectedInput));
assertFalse(Files.exists(expectedOutput));
assertEquals(workingDir, deletedDir.get());
Mockito.verify(tempFileManager).deleteTempDirectory(workingDir);
}
if (Files.exists(workingDir)) {
try (Stream<Path> paths = Files.walk(workingDir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
}
@Test
void convertEbookToPdf_withUnsupportedExtensionThrows() {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile unsupported =
new MockMultipartFile(
"fileInput", "ebook.exe", "application/octet-stream", new byte[] {1, 2, 3});
ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest();
request.setFileInput(unsupported);
assertThrows(IllegalArgumentException.class, () -> controller.convertEbookToPdf(request));
}
@Test
void convertEbookToPdf_withOptimizeForEbookUsesGhostscript() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(true);
MockMultipartFile ebookFile =
new MockMultipartFile(
"fileInput", "ebook.epub", "application/epub+zip", "content".getBytes());
ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest();
request.setFileInput(ebookFile);
request.setOptimizeForEbook(true);
Path workingDir = Files.createTempDirectory("ebook-convert-opt-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
AtomicReference<Path> deletedDir = new AtomicReference<>();
Mockito.doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
deletedDir.set(dir);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class)) {
ProcessExecutor executor = Mockito.mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = Mockito.mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
Path expectedInput = workingDir.resolve("ebook.epub");
Path expectedOutput = workingDir.resolve("ebook.pdf");
when(executor.runCommandWithOutputHandling(any(List.class), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "pdf");
return execResult;
});
gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf"))
.thenReturn("ebook_convertedToPDF.pdf");
byte[] optimizedBytes = "optimized".getBytes(StandardCharsets.UTF_8);
gu.when(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class)))
.thenReturn(optimizedBytes);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(optimizedBytes);
wr.when(
() ->
WebResponseUtils.bytesToWebResponse(
optimizedBytes, "ebook_convertedToPDF.pdf"))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertEbookToPdf(request);
assertSame(expectedResponse, response);
gu.verify(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class)));
Mockito.verifyNoInteractions(pdfDocumentFactory);
Mockito.verify(tempFileManager).deleteTempDirectory(workingDir);
assertEquals(workingDir, deletedDir.get());
assertFalse(Files.exists(expectedInput));
assertFalse(Files.exists(expectedOutput));
}
if (Files.exists(workingDir)) {
try (Stream<Path> paths = Files.walk(workingDir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
}
}
@@ -0,0 +1,328 @@
package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
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.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.OutputFormat;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.TargetDevice;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.ProcessExecutor.Processes;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class ConvertPDFToEpubControllerTest {
private static final MediaType EPUB_MEDIA_TYPE = MediaType.valueOf("application/epub+zip");
@Mock private TempFileManager tempFileManager;
@Mock private EndpointConfiguration endpointConfiguration;
@InjectMocks private ConvertPDFToEpubController controller;
@Test
void convertPdfToEpub_buildsGoldenCommandAndCleansUp() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile pdfFile =
new MockMultipartFile(
"fileInput", "novel.pdf", "application/pdf", "content".getBytes());
ConvertPdfToEpubRequest request = new ConvertPdfToEpubRequest();
request.setFileInput(pdfFile);
Path workingDir = Files.createTempDirectory("pdf-epub-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
AtomicReference<Path> deletedDir = new AtomicReference<>();
doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
deletedDir.set(dir);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
ProcessExecutor executor = mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
Path expectedInput = workingDir.resolve("novel.pdf");
Path expectedOutput = workingDir.resolve("novel.epub");
when(executor.runCommandWithOutputHandling(
commandCaptor.capture(), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "epub");
return execResult;
});
gu.when(() -> GeneralUtils.generateFilename("novel.pdf", "_convertedToEPUB.epub"))
.thenReturn("novel_convertedToEPUB.epub");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertEquals(11, command.size());
assertEquals("ebook-convert", command.get(0));
assertEquals(expectedInput.toString(), command.get(1));
assertEquals(expectedOutput.toString(), command.get(2));
assertTrue(command.contains("--enable-heuristics"));
assertTrue(command.contains("--insert-blank-line"));
assertTrue(command.contains("--filter-css"));
assertTrue(
command.contains(
"font-family,color,background-color,margin-left,margin-right"));
assertTrue(command.contains("--chapter"));
assertTrue(command.stream().anyMatch(arg -> arg.contains("Chapter\\s+")));
assertTrue(command.contains("--output-profile"));
assertTrue(command.contains(TargetDevice.TABLET_PHONE_IMAGES.getCalibreProfile()));
assertEquals(EPUB_MEDIA_TYPE, response.getHeaders().getContentType());
assertEquals(
"novel_convertedToEPUB.epub",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8));
verify(tempFileManager).deleteTempDirectory(workingDir);
assertEquals(workingDir, deletedDir.get());
} finally {
deleteIfExists(workingDir);
}
}
@Test
void convertPdfToEpub_respectsOptions() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile pdfFile =
new MockMultipartFile(
"fileInput", "story.pdf", "application/pdf", "content".getBytes());
ConvertPdfToEpubRequest request = new ConvertPdfToEpubRequest();
request.setFileInput(pdfFile);
request.setDetectChapters(false);
request.setTargetDevice(TargetDevice.KINDLE_EINK_TEXT);
Path workingDir = Files.createTempDirectory("pdf-epub-options-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
ProcessExecutor executor = mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
Path expectedOutput = workingDir.resolve("story.epub");
when(executor.runCommandWithOutputHandling(
commandCaptor.capture(), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "epub");
return execResult;
});
gu.when(() -> GeneralUtils.generateFilename("story.pdf", "_convertedToEPUB.epub"))
.thenReturn("story_convertedToEPUB.epub");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertTrue(command.stream().noneMatch(arg -> "--chapter".equals(arg)));
assertTrue(command.contains("--output-profile"));
assertTrue(command.contains(TargetDevice.KINDLE_EINK_TEXT.getCalibreProfile()));
assertTrue(command.contains("--filter-css"));
assertTrue(
command.contains(
"font-family,color,background-color,margin-left,margin-right"));
assertTrue(command.size() >= 9);
assertEquals(EPUB_MEDIA_TYPE, response.getHeaders().getContentType());
assertEquals(
"story_convertedToEPUB.epub",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8));
} finally {
deleteIfExists(workingDir);
}
}
@Test
void convertPdfToAzw3_buildsCorrectCommandAndOutput() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile pdfFile =
new MockMultipartFile(
"fileInput", "book.pdf", "application/pdf", "content".getBytes());
ConvertPdfToEpubRequest request = new ConvertPdfToEpubRequest();
request.setFileInput(pdfFile);
request.setOutputFormat(OutputFormat.AZW3);
request.setDetectChapters(false);
request.setTargetDevice(TargetDevice.KINDLE_EINK_TEXT);
Path workingDir = Files.createTempDirectory("pdf-azw3-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
ProcessExecutor executor = mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
Path expectedInput = workingDir.resolve("book.pdf");
Path expectedOutput = workingDir.resolve("book.azw3");
when(executor.runCommandWithOutputHandling(
commandCaptor.capture(), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "azw3");
return execResult;
});
gu.when(() -> GeneralUtils.generateFilename("book.pdf", "_convertedToAZW3.azw3"))
.thenReturn("book_convertedToAZW3.azw3");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertEquals("ebook-convert", command.get(0));
assertEquals(expectedInput.toString(), command.get(1));
assertEquals(expectedOutput.toString(), command.get(2));
assertTrue(command.contains("--enable-heuristics"));
assertTrue(command.contains("--insert-blank-line"));
assertTrue(command.contains("--filter-css"));
assertTrue(command.stream().noneMatch(arg -> "--chapter".equals(arg)));
assertTrue(command.contains("--output-profile"));
assertTrue(command.contains(TargetDevice.KINDLE_EINK_TEXT.getCalibreProfile()));
assertEquals(
MediaType.valueOf("application/vnd.amazon.ebook"),
response.getHeaders().getContentType());
assertEquals(
"book_convertedToAZW3.azw3",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("azw3", new String(response.getBody(), StandardCharsets.UTF_8));
verify(tempFileManager).deleteTempDirectory(workingDir);
} finally {
deleteIfExists(workingDir);
}
}
private void deleteIfExists(Path directory) throws IOException {
if (directory == null || !Files.exists(directory)) {
return;
}
try (Stream<Path> paths = Files.walk(directory)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
}
@@ -0,0 +1,571 @@
package stirling.software.SPDF.controller.api.converters;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.preflight.ValidationResult;
import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.schema.DublinCoreSchema;
import org.apache.xmpbox.schema.PDFAIdentificationSchema;
import org.apache.xmpbox.schema.XMPBasicSchema;
import org.apache.xmpbox.xml.DomXmpParser;
import org.apache.xmpbox.xml.XmpSerializer;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
@DisplayName("PDF to PDF/A Converter Tests")
@ExtendWith(MockitoExtension.class)
class ConvertPDFToPDFATest {
@TempDir Path tempDir;
@SuppressWarnings("unchecked")
private static <T> T invokePrivateMethod(String methodName, Object... args) throws Exception {
Class<?>[] paramTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] == null) {
paramTypes[i] = Object.class;
} else if (args[i] instanceof Integer) {
paramTypes[i] = int.class;
} else if (args[i] instanceof Boolean) {
paramTypes[i] = boolean.class;
} else {
paramTypes[i] = args[i].getClass();
}
}
try {
Method method = ConvertPDFToPDFA.class.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
return (T) method.invoke(null, args);
} catch (NoSuchMethodException e) {
for (Method method : ConvertPDFToPDFA.class.getDeclaredMethods()) {
if (method.getName().equals(methodName)
&& method.getParameterCount() == args.length) {
method.setAccessible(true);
return (T) method.invoke(null, args);
}
}
throw e;
}
}
private PDDocument createSimplePdf() throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
contentStream.newLineAtOffset(100, 700);
contentStream.showText("Test PDF Document");
contentStream.endText();
}
return document;
}
private PDDocument createPdfWithMetadata(String title, String author, String creator)
throws IOException {
PDDocument document = createSimplePdf();
PDDocumentInformation info = new PDDocumentInformation();
info.setTitle(title);
info.setAuthor(author);
info.setCreator(creator);
info.setSubject("Test Subject");
info.setKeywords("test, pdf, metadata");
info.setProducer("Test Producer");
GregorianCalendar cal = new GregorianCalendar(2024, Calendar.JANUARY, 1);
info.setCreationDate(cal);
info.setModificationDate(cal);
document.setDocumentInformation(info);
return document;
}
private PDDocument createPdfWithTransparency() throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics2D g2d = bufferedImage.createGraphics();
g2d.setColor(new Color(255, 0, 0, 128)); // Semi-transparent red
g2d.fillRect(0, 0, 100, 100);
g2d.dispose();
PDImageXObject image = LosslessFactory.createFromImage(document, bufferedImage);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(image, 100, 600, 100, 100);
}
return document;
}
private PDDocument createPdfWithXmpMetadata(int pdfaPart) throws Exception {
PDDocument document = createSimplePdf();
XMPMetadata xmp = XMPMetadata.createXMPMetadata();
PDFAIdentificationSchema pdfaSchema = xmp.createAndAddPDFAIdentificationSchema();
pdfaSchema.setPart(pdfaPart);
pdfaSchema.setConformance("B");
DublinCoreSchema dcSchema = xmp.createAndAddDublinCoreSchema();
dcSchema.addCreator("Test Creator");
dcSchema.setTitle("Test Title");
XMPBasicSchema xmpBasicSchema = xmp.createAndAddXMPBasicSchema();
xmpBasicSchema.setCreatorTool("Test Tool");
ByteArrayOutputStream xmpStream = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmp, xmpStream, true);
PDMetadata metadata = new PDMetadata(document);
metadata.importXMPMetadata(xmpStream.toByteArray());
document.getDocumentCatalog().setMetadata(metadata);
return document;
}
@Nested
@DisplayName("XMP Metadata Operations")
class XmpMetadataTests {
@Test
@DisplayName("Should add PDF/A-1 identification schema to XMP metadata")
void shouldAddPdfA1IdentificationSchema() throws Exception {
PDDocument document = createPdfWithMetadata("Test PDF", "Test Author", "Test Creator");
invokePrivateMethod("mergeAndAddXmpMetadata", document, 1);
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
assertThat(metadata).isNotNull();
try (InputStream is = metadata.createInputStream()) {
DomXmpParser parser = new DomXmpParser();
XMPMetadata xmp = parser.parse(is);
PDFAIdentificationSchema pdfaSchema =
(PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class);
assertThat(pdfaSchema).isNotNull();
assertThat(pdfaSchema.getPart()).isEqualTo(1);
assertThat(pdfaSchema.getConformance()).isEqualTo("B");
}
document.close();
}
@Test
@DisplayName("Should add PDF/A-2 identification schema to XMP metadata")
void shouldAddPdfA2IdentificationSchema() throws Exception {
PDDocument document = createSimplePdf();
invokePrivateMethod("mergeAndAddXmpMetadata", document, 2);
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
try (InputStream is = metadata.createInputStream()) {
DomXmpParser parser = new DomXmpParser();
XMPMetadata xmp = parser.parse(is);
PDFAIdentificationSchema pdfaSchema =
(PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class);
assertThat(pdfaSchema.getPart()).isEqualTo(2);
assertThat(pdfaSchema.getConformance()).isEqualTo("B");
}
document.close();
}
@Test
@DisplayName("Should preserve Dublin Core creator information")
void shouldPreserveDublinCoreCreatorInformation() throws Exception {
PDDocument document =
createPdfWithMetadata("Test PDF", "Test Author", "Original Creator");
invokePrivateMethod("mergeAndAddXmpMetadata", document, 1);
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
try (InputStream is = metadata.createInputStream()) {
DomXmpParser parser = new DomXmpParser();
XMPMetadata xmp = parser.parse(is);
DublinCoreSchema dcSchema = xmp.getDublinCoreSchema();
assertThat(dcSchema).isNotNull();
assertThat(dcSchema.getCreators()).contains("Original Creator");
}
document.close();
}
@Test
@DisplayName("Should set creation and modification timestamps")
void shouldSetCreationAndModificationTimestamps() throws Exception {
PDDocument document = createSimplePdf();
invokePrivateMethod("mergeAndAddXmpMetadata", document, 1);
PDDocumentInformation info = document.getDocumentInformation();
assertThat(info.getCreationDate()).isNotNull();
assertThat(info.getModificationDate()).isNotNull();
document.close();
}
@Test
@DisplayName("Should handle existing XMP metadata gracefully")
void shouldHandleExistingXmpMetadata() throws Exception {
PDDocument document = createPdfWithXmpMetadata(1);
invokePrivateMethod("mergeAndAddXmpMetadata", document, 2);
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
try (InputStream is = metadata.createInputStream()) {
DomXmpParser parser = new DomXmpParser();
XMPMetadata xmp = parser.parse(is);
PDFAIdentificationSchema pdfaSchema =
(PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class);
assertThat(pdfaSchema.getPart()).isEqualTo(2);
}
document.close();
}
}
@Nested
@DisplayName("Content Sanitization")
class ContentSanitizationTests {
@Test
@DisplayName("Should verify COSDictionary JavaScript removal logic")
void shouldVerifyJavaScriptRemovalLogic() throws Exception {
COSDictionary dict = new COSDictionary();
dict.setString(COSName.JAVA_SCRIPT, "app.alert('test');");
dict.setString(COSName.getPDFName("JS"), "some_js_code");
assertThat(dict.containsKey(COSName.JAVA_SCRIPT)).isTrue();
assertThat(dict.containsKey(COSName.getPDFName("JS"))).isTrue();
invokePrivateMethod("sanitizePdfA", dict, 1);
assertThat(dict.containsKey(COSName.JAVA_SCRIPT)).isFalse();
assertThat(dict.containsKey(COSName.getPDFName("JS"))).isFalse();
}
@Test
@DisplayName("Should verify interpolation is set to false")
void shouldVerifyInterpolationSetToFalse() throws Exception {
COSDictionary dict = new COSDictionary();
dict.setBoolean(COSName.INTERPOLATE, true);
assertThat(dict.getBoolean(COSName.INTERPOLATE, false)).isTrue();
invokePrivateMethod("sanitizePdfA", dict, 1);
assertThat(dict.getBoolean(COSName.INTERPOLATE, true)).isFalse();
}
@Test
@DisplayName("Should verify SMask removal for PDF/A-1")
void shouldVerifySMaskRemovalForPdfA1() throws Exception {
COSDictionary dict = new COSDictionary();
dict.setItem(COSName.SMASK, new COSArray());
assertThat(dict.containsKey(COSName.SMASK)).isTrue();
invokePrivateMethod("sanitizePdfA", dict, 1);
assertThat(dict.containsKey(COSName.SMASK)).isFalse();
}
@Test
@DisplayName("Should verify transparency group removal for PDF/A-1")
void shouldVerifyTransparencyGroupRemovalForPdfA1() throws Exception {
COSDictionary dict = new COSDictionary();
COSDictionary groupDict = new COSDictionary();
groupDict.setItem(COSName.S, COSName.TRANSPARENCY);
dict.setItem(COSName.GROUP, groupDict);
assertThat(dict.containsKey(COSName.GROUP)).isTrue();
invokePrivateMethod("sanitizePdfA", dict, 1);
assertThat(dict.containsKey(COSName.GROUP)).isFalse();
}
@Test
@DisplayName("Should verify forbidden elements are removed")
void shouldVerifyForbiddenElementsRemoved() throws Exception {
COSDictionary dict = new COSDictionary();
dict.setItem(COSName.URI, COSName.A);
dict.setItem(COSName.EMBEDDED_FILES, new COSArray());
dict.setItem(COSName.FILESPEC, new COSDictionary());
dict.setItem(COSName.getPDFName("RichMedia"), new COSDictionary());
assertThat(dict.containsKey(COSName.URI)).isTrue();
assertThat(dict.containsKey(COSName.EMBEDDED_FILES)).isTrue();
invokePrivateMethod("sanitizePdfA", dict, 1);
assertThat(dict.containsKey(COSName.URI)).isFalse();
assertThat(dict.containsKey(COSName.EMBEDDED_FILES)).isFalse();
assertThat(dict.containsKey(COSName.FILESPEC)).isFalse();
assertThat(dict.containsKey(COSName.getPDFName("RichMedia"))).isFalse();
}
}
@Nested
@DisplayName("Transparency Detection")
class TransparencyDetectionTests {
@Test
@DisplayName("Should detect SMask transparency")
void shouldDetectSMaskTransparency() throws Exception {
PDDocument document = createPdfWithTransparency();
boolean hasTransparency = invokePrivateMethod("hasTransparentImages", document);
assertThat(hasTransparency).isTrue();
document.close();
}
@Test
@DisplayName("Should not detect transparency in opaque images")
void shouldNotDetectTransparencyInOpaqueImages() throws Exception {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D g2d = bufferedImage.createGraphics();
g2d.setColor(Color.RED);
g2d.fillRect(0, 0, 100, 100);
g2d.dispose();
PDImageXObject image = LosslessFactory.createFromImage(document, bufferedImage);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(image, 100, 600, 100, 100);
}
boolean hasTransparency = invokePrivateMethod("hasTransparentImages", document);
assertThat(hasTransparency).isFalse();
document.close();
}
@Test
@DisplayName("Should detect interpolation flag")
void shouldDetectInterpolationFlag() throws Exception {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
PDImageXObject image = LosslessFactory.createFromImage(document, bufferedImage);
image.setInterpolate(true);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(image, 100, 600);
}
boolean hasTransparency = invokePrivateMethod("hasTransparentImages", document);
assertThat(hasTransparency).isTrue();
document.close();
}
}
@Nested
@DisplayName("Color Profile Management")
class ColorProfileTests {
@Test
@DisplayName("Should verify ICC profile can be loaded from resources")
void shouldVerifyIccProfileCanBeLoadedFromResources() throws Exception {
try (InputStream iccStream = getClass().getResourceAsStream("/icc/sRGB2014.icc")) {
assertThat(iccStream).isNotNull();
byte[] iccData = iccStream.readAllBytes();
assertThat(iccData).isNotEmpty();
assertThat(iccData).hasSizeGreaterThan(1000);
}
}
@Test
@DisplayName("Should create color profile output intent structure")
void shouldCreateColorProfileOutputIntentStructure() throws Exception {
PDDocument document = createSimplePdf();
try (InputStream iccStream = getClass().getResourceAsStream("/icc/sRGB2014.icc")) {
if (iccStream != null) {
PDOutputIntent outputIntent = new PDOutputIntent(document, iccStream);
outputIntent.setInfo("sRGB IEC61966-2.1");
outputIntent.setOutputCondition("sRGB");
outputIntent.setOutputConditionIdentifier("sRGB IEC61966-2.1");
outputIntent.setRegistryName("http://www.color.org");
document.getDocumentCatalog().addOutputIntent(outputIntent);
assertThat(document.getDocumentCatalog().getOutputIntents()).hasSize(1);
PDOutputIntent retrieved =
document.getDocumentCatalog().getOutputIntents().get(0);
assertThat(retrieved.getInfo()).contains("sRGB");
}
}
document.close();
}
}
@Nested
@DisplayName("Validation")
class ValidationTests {
@Test
@DisplayName("Should format validation errors correctly")
void shouldFormatValidationErrorsCorrectly() {
String errorCode = "ERROR_CODE_123";
String errorDetails = "Missing XMP metadata";
assertThat(errorCode).isNotBlank();
assertThat(errorDetails).contains("XMP");
assertThat(errorCode).startsWith("ERROR");
}
@Test
@DisplayName("Should handle validation error details")
void shouldHandleValidationErrorDetails() {
String error1Code = "1.2.3";
String error1Detail = "Font not embedded";
String error2Detail = "Missing color profile";
assertThat(error1Code).matches("\\d+\\.\\d+\\.\\d+");
assertThat(error1Detail).contains("Font");
assertThat(error2Detail).contains("color profile");
}
@Test
@DisplayName("Should create validation result with errors")
void shouldCreateValidationResultWithErrors() {
ValidationResult result = new ValidationResult(false);
assertThat(result.isValid()).isFalse();
assertThat(result.getErrorsList()).isNotNull();
}
}
@Nested
@DisplayName("Helper Methods")
class HelperMethodsTests {
@Test
@DisplayName("Should build standard Type1 glyph set")
void shouldBuildStandardType1GlyphSet() throws Exception {
String glyphSet = invokePrivateMethod("buildStandardType1GlyphSet");
assertThat(glyphSet).isNotBlank().contains("space", "A", "a", "zero", "period");
}
@Test
@DisplayName("Should delete directory recursively")
void shouldDeleteDirectoryRecursively() throws Exception {
Path testDir = tempDir.resolve("test_delete");
Files.createDirectories(testDir);
Path subDir = testDir.resolve("subdir");
Files.createDirectories(subDir);
Files.createFile(testDir.resolve("file1.txt"));
Files.createFile(subDir.resolve("file2.txt"));
assertThat(Files.exists(testDir)).isTrue();
invokePrivateMethod("deleteQuietly", testDir);
assertThat(Files.exists(testDir)).isFalse();
}
@Test
@DisplayName("Should handle null path in deleteQuietly")
void shouldHandleNullPathInDeleteQuietly() {
assertDoesNotThrow(() -> invokePrivateMethod("deleteQuietly", (Path) null));
}
@Test
@DisplayName("Should handle non-existent path in deleteQuietly")
void shouldHandleNonExistentPathInDeleteQuietly() {
Path nonExistent = tempDir.resolve("non_existent_dir");
assertDoesNotThrow(() -> invokePrivateMethod("deleteQuietly", nonExistent));
}
}
@Nested
@DisplayName("Error Handling")
class ErrorHandlingTests {
@Test
@DisplayName("Should handle empty PDF document")
void shouldHandleEmptyPdfDocument() {
PDDocument document = new PDDocument();
assertDoesNotThrow(
() -> {
invokePrivateMethod("mergeAndAddXmpMetadata", document, 1);
document.close();
});
}
@Test
@DisplayName("Should handle PDF with no resources")
void shouldHandlePdfWithNoResources() throws Exception {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
assertThat(page.getResources()).isNull();
COSDictionary simpleDict = new COSDictionary();
simpleDict.setItem(COSName.JAVA_SCRIPT, COSName.A);
assertDoesNotThrow(
() -> {
invokePrivateMethod("sanitizePdfA", simpleDict, 1);
});
assertThat(simpleDict.containsKey(COSName.JAVA_SCRIPT)).isFalse();
document.close();
}
}
}
@@ -3,14 +3,19 @@ package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -51,18 +56,18 @@ public class ConvertWebsiteToPdfTest {
void setUp() throws Exception {
mocks = MockitoAnnotations.openMocks(this);
// Feature einschalten (ggf. Struktur an dein Projekt anpassen)
// Enable feature (adjust structure for your project if necessary)
applicationProperties = new ApplicationProperties();
applicationProperties.getSystem().setEnableUrlToPDF(true);
// Stubs, falls der Code weiterlaufen sollte
// Stubs in case the code continues to run
when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/usr/bin/weasyprint");
when(pdfDocumentFactory.load(any(File.class))).thenReturn(new PDDocument());
// SUT bauen
// Build SUT
sut = new ConvertWebsiteToPDF(pdfDocumentFactory, runtimePathConfig, applicationProperties);
// RequestContext für ServletUriComponentsBuilder bereitstellen
// Provide RequestContext for ServletUriComponentsBuilder
MockHttpServletRequest req = new MockHttpServletRequest();
req.setScheme("http");
req.setServerName("localhost");
@@ -94,7 +99,7 @@ public class ConvertWebsiteToPdfTest {
@Test
void redirect_with_error_when_url_is_not_reachable() throws Exception {
UrlToPdfRequest request = new UrlToPdfRequest();
// .invalid ist per RFC reserviert und nicht auflösbar
// .invalid is reserved by RFC and not resolvable
request.setUrlInput("https://nonexistent.invalid/");
ResponseEntity<?> resp = sut.urlToPdf(request);
@@ -109,7 +114,7 @@ public class ConvertWebsiteToPdfTest {
@Test
void redirect_with_error_when_endpoint_disabled() throws Exception {
// Feature deaktivieren
// Disable feature
applicationProperties.getSystem().setEnableUrlToPDF(false);
UrlToPdfRequest request = new UrlToPdfRequest();
@@ -135,9 +140,9 @@ public class ConvertWebsiteToPdfTest {
String out = (String) m.invoke(sut, in);
assertTrue(out.endsWith(".pdf"));
// Nur AZ, az, 09, Unterstrich und Punkt erlaubt
// Only AZ, az, 09, underscore and dot allowed
assertTrue(out.matches("[A-Za-z0-9_]+\\.pdf"));
// keine Truncation hier (Quelle ist nicht so lang)
// no truncation here (source not that long)
assertTrue(out.length() <= 54);
}
@@ -147,14 +152,14 @@ public class ConvertWebsiteToPdfTest {
ConvertWebsiteToPDF.class.getDeclaredMethod("convertURLToFileName", String.class);
m.setAccessible(true);
// Sehr lange URL → löst Truncation aus
// Very long URL -> triggers truncation
String longUrl =
"https://very-very-long-domain.example.com/some/really/long/path/with?many=params&and=chars";
String out = (String) m.invoke(sut, longUrl);
assertTrue(out.endsWith(".pdf"));
assertTrue(out.matches("[A-Za-z0-9_]+\\.pdf"));
// safeName ist auf 50 begrenzt → total max 54 inkl. ".pdf"
// safeName limited to 50 -> total max 54 including '.pdf'
assertTrue(out.length() <= 54, "Filename should be truncated to 50 + '.pdf'");
}
@@ -165,25 +170,26 @@ public class ConvertWebsiteToPdfTest {
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
MockedStatic<HttpClient> httpClient = mockHttpClientReturning("<html></html>")) {
// URL-Checks positiv erzwingen
// Force URL checks to be positive
gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true);
gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true);
// richtiger ProcessExecutor!
// correct ProcessExecutor!
ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.WEASYPRINT)).thenReturn(mockExec);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> cmdCaptor = ArgumentCaptor.forClass(List.class);
// Rückgabewert typgerecht
// Return value of correct type
ProcessExecutorResult dummyResult = Mockito.mock(ProcessExecutorResult.class);
when(mockExec.runCommandWithOutputHandling(cmdCaptor.capture()))
.thenReturn(dummyResult);
// WebResponseUtils mocken
// Mock WebResponseUtils
ResponseEntity<byte[]> fakeResponse = ResponseEntity.ok(new byte[0]);
wr.when(() -> WebResponseUtils.pdfDocToWebResponse(any(PDDocument.class), anyString()))
.thenReturn(fakeResponse);
@@ -194,20 +200,22 @@ public class ConvertWebsiteToPdfTest {
// Assert Response OK
assertEquals(HttpStatus.OK, resp.getStatusCode());
// Assert WeasyPrint-Kommando korrekt
// Assert WeasyPrint command correct
List<String> cmd = cmdCaptor.getValue();
assertNotNull(cmd);
assertEquals("/usr/bin/weasyprint", cmd.get(0));
assertEquals("https://example.com", cmd.get(1));
assertEquals("--pdf-forms", cmd.get(2));
assertTrue(cmd.size() >= 4, "WeasyPrint sollte einen Output-Pfad erhalten");
String outPathStr = cmd.get(3);
assertTrue(cmd.size() >= 6, "WeasyPrint should receive HTML input and output path");
String htmlPathStr = cmd.get(1);
assertEquals("--base-url", cmd.get(2));
assertEquals("https://example.com", cmd.get(3));
assertEquals("--pdf-forms", cmd.get(4));
String outPathStr = cmd.get(5);
assertNotNull(outPathStr);
// Temp-Datei muss im finally gelöscht sein
Path outPath = Path.of(outPathStr);
// Temp file must be deleted in finally
assertFalse(
Files.exists(outPath), "Temp-Output-Datei sollte nach dem Call gelöscht sein");
Files.exists(Path.of(htmlPathStr)),
"Temp HTML file should be deleted after the call");
}
}
@@ -217,22 +225,33 @@ public class ConvertWebsiteToPdfTest {
UrlToPdfRequest request = new UrlToPdfRequest();
request.setUrlInput("https://example.com");
Path preCreatedTemp = java.nio.file.Files.createTempFile("test_output_", ".pdf");
Path preCreatedTemp = Files.createTempFile("test_output_", ".pdf");
Path htmlTemp = Files.createTempFile("test_input_", ".html");
try (MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class);
MockedStatic<Files> files = Mockito.mockStatic(Files.class)) {
MockedStatic<Files> files = Mockito.mockStatic(Files.class);
MockedStatic<HttpClient> httpClient = mockHttpClientReturning("<html></html>")) {
// URL-Checks positiv
// Force URL checks to be positive
gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true);
gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true);
// Temp-Datei erzwingen + Delete-Fehler provozieren
// Force temp files + provoke delete error
files.when(() -> Files.createTempFile("url_input_", ".html")).thenReturn(htmlTemp);
files.when(() -> Files.createTempFile("output_", ".pdf")).thenReturn(preCreatedTemp);
files.when(
() ->
Files.writeString(
eq(htmlTemp),
anyString(),
eq(java.nio.charset.StandardCharsets.UTF_8)))
.thenReturn(htmlTemp);
files.when(() -> Files.deleteIfExists(htmlTemp)).thenReturn(true);
files.when(() -> Files.deleteIfExists(preCreatedTemp))
.thenThrow(new IOException("fail delete"));
files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true); // für den Assert
files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true); // for the assert
// ProcessExecutor
ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class);
@@ -245,20 +264,63 @@ public class ConvertWebsiteToPdfTest {
wr.when(() -> WebResponseUtils.pdfDocToWebResponse(any(PDDocument.class), anyString()))
.thenReturn(fakeResponse);
// Act: darf keine Exception werfen und soll eine Response liefern
// Act: should not throw and should return a Response
ResponseEntity<?> resp = assertDoesNotThrow(() -> sut.urlToPdf(request));
// Assert
assertNotNull(resp, "Response should not be null");
assertEquals(HttpStatus.OK, resp.getStatusCode());
assertTrue(
java.nio.file.Files.exists(preCreatedTemp),
"Temp-Datei sollte trotz Lösch-IOException noch existieren");
Files.exists(preCreatedTemp),
"Temp file should still exist despite delete IOException");
} finally {
try {
java.nio.file.Files.deleteIfExists(preCreatedTemp);
Files.deleteIfExists(preCreatedTemp);
Files.deleteIfExists(htmlTemp);
} catch (IOException ignore) {
}
}
}
private static MockedStatic<HttpClient> mockHttpClientReturning(String body) throws Exception {
MockedStatic<HttpClient> httpClientStatic = Mockito.mockStatic(HttpClient.class);
HttpClient.Builder builder = Mockito.mock(HttpClient.Builder.class);
HttpClient client = Mockito.mock(HttpClient.class);
HttpResponse<String> response = Mockito.mock();
httpClientStatic.when(HttpClient::newBuilder).thenReturn(builder);
when(builder.followRedirects(HttpClient.Redirect.NORMAL)).thenReturn(builder);
when(builder.connectTimeout(any(Duration.class))).thenReturn(builder);
when(builder.build()).thenReturn(client);
Mockito.doReturn(response).when(client).send(any(HttpRequest.class), any());
when(response.statusCode()).thenReturn(200);
when(response.body()).thenReturn(body);
return httpClientStatic;
}
@Test
void redirect_with_error_when_disallowed_content_detected() throws Exception {
UrlToPdfRequest request = new UrlToPdfRequest();
request.setUrlInput("https://example.com");
try (MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
MockedStatic<HttpClient> httpClient =
mockHttpClientReturning(
"<link rel=\"attachment\" href=\"file:///etc/passwd\">")) {
gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true);
gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true);
ResponseEntity<?> resp = sut.urlToPdf(request);
assertEquals(HttpStatus.SEE_OTHER, resp.getStatusCode());
URI location = resp.getHeaders().getLocation();
assertNotNull(location, "Location header expected");
assertTrue(
location.getQuery() != null
&& location.getQuery().contains("error=error.disallowedUrlContent"));
}
}
}
@@ -42,9 +42,7 @@ public class PdfToCbzUtilsTest {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(null, 300, pdfDocumentFactory);
});
() -> PdfToCbzUtils.convertPdfToCbz(null, 300, pdfDocumentFactory));
Assertions.assertEquals("File cannot be null or empty", exception.getMessage());
}
@@ -56,9 +54,7 @@ public class PdfToCbzUtilsTest {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(emptyFile, 300, pdfDocumentFactory);
});
() -> PdfToCbzUtils.convertPdfToCbz(emptyFile, 300, pdfDocumentFactory));
Assertions.assertEquals("File cannot be null or empty", exception.getMessage());
}
@@ -70,10 +66,8 @@ public class PdfToCbzUtilsTest {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(nonPdfFile, 300, pdfDocumentFactory);
});
Assertions.assertEquals("File must be a PDF", exception.getMessage());
() -> PdfToCbzUtils.convertPdfToCbz(nonPdfFile, 300, pdfDocumentFactory));
Assertions.assertEquals("File must be in PDF format", exception.getMessage());
}
@Test
@@ -90,9 +84,7 @@ public class PdfToCbzUtilsTest {
// structure
Assertions.assertThrows(
Exception.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(pdfFile, 300, pdfDocumentFactory);
});
() -> PdfToCbzUtils.convertPdfToCbz(pdfFile, 300, pdfDocumentFactory));
// Verify that load was called
Mockito.verify(pdfDocumentFactory).load(pdfFile);
@@ -0,0 +1,146 @@
package stirling.software.SPDF.controller.api.converters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
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.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class PdfVectorExportControllerTest {
private final List<Path> tempPaths = new ArrayList<>();
@Mock private TempFileManager tempFileManager;
@Mock private EndpointConfiguration endpointConfiguration;
@Mock private ProcessExecutor ghostscriptExecutor;
@InjectMocks private PdfVectorExportController controller;
private Map<ProcessExecutor.Processes, ProcessExecutor> originalExecutors;
@BeforeEach
void setup() throws Exception {
when(tempFileManager.createTempFile(any()))
.thenAnswer(
invocation -> {
String suffix = invocation.<String>getArgument(0);
Path path =
Files.createTempFile(
"vector_test", suffix == null ? "" : suffix);
tempPaths.add(path);
return path.toFile();
});
Field instancesField = ProcessExecutor.class.getDeclaredField("instances");
instancesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<ProcessExecutor.Processes, ProcessExecutor> instances =
(Map<ProcessExecutor.Processes, ProcessExecutor>) instancesField.get(null);
originalExecutors = Map.copyOf(instances);
instances.clear();
instances.put(ProcessExecutor.Processes.GHOSTSCRIPT, ghostscriptExecutor);
}
@AfterEach
void tearDown() throws Exception {
Field instancesField = ProcessExecutor.class.getDeclaredField("instances");
instancesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<ProcessExecutor.Processes, ProcessExecutor> instances =
(Map<ProcessExecutor.Processes, ProcessExecutor>) instancesField.get(null);
instances.clear();
if (originalExecutors != null) {
instances.putAll(originalExecutors);
}
reset(ghostscriptExecutor, tempFileManager, endpointConfiguration);
for (Path path : tempPaths) {
Files.deleteIfExists(path);
}
tempPaths.clear();
}
private ProcessExecutorResult mockResult(int rc) {
ProcessExecutorResult result = mock(ProcessExecutorResult.class);
lenient().when(result.getRc()).thenReturn(rc);
lenient().when(result.getMessages()).thenReturn("");
return result;
}
@Test
void convertGhostscript_psToPdf_success() throws Exception {
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(true);
ProcessExecutorResult result = mockResult(0);
when(ghostscriptExecutor.runCommandWithOutputHandling(any())).thenReturn(result);
MockMultipartFile file =
new MockMultipartFile(
"fileInput",
"sample.ps",
MediaType.APPLICATION_OCTET_STREAM_VALUE,
new byte[] {1});
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertGhostscriptInputsToPdf(request);
assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF);
}
@Test
void convertGhostscript_pdfPassThrough_success() throws Exception {
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(false);
byte[] content = {1};
MockMultipartFile file =
new MockMultipartFile(
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, content);
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertGhostscriptInputsToPdf(request);
assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF);
assertThat(response.getBody()).contains(content);
}
@Test
void convertGhostscript_unsupportedFormatThrows() {
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(false);
MockMultipartFile file =
new MockMultipartFile(
"fileInput", "vector.svg", MediaType.APPLICATION_XML_VALUE, new byte[] {1});
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
assertThrows(
IllegalArgumentException.class,
() -> controller.convertGhostscriptInputsToPdf(request));
}
}
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api.pipeline;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@@ -50,13 +49,7 @@ class PipelineProcessorTest {
PipelineConfig config = new PipelineConfig();
config.setOperations(List.of(op));
Resource file =
new ByteArrayResource("data".getBytes()) {
@Override
public String getFilename() {
return "test.pdf";
}
};
Resource file = new MyFileByteArrayResource();
List<Resource> files = List.of(file);
@@ -78,4 +71,15 @@ class PipelineProcessorTest {
assertFalse(result.isHasErrors(), "No errors should occur");
assertTrue(result.getOutputFiles().isEmpty(), "Filtered file list should be empty");
}
private static class MyFileByteArrayResource extends ByteArrayResource {
public MyFileByteArrayResource() {
super("data".getBytes());
}
@Override
public String getFilename() {
return "test.pdf";
}
}
}
@@ -0,0 +1,843 @@
package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.GregorianCalendar;
import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.ProtectionPolicy;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
@DisplayName("GetInfoOnPDF Controller Tests")
@ExtendWith(MockitoExtension.class)
class GetInfoOnPDFTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@InjectMocks private GetInfoOnPDF getInfoOnPDF;
private ObjectMapper objectMapper;
private static final java.time.ZonedDateTime FIXED_NOW =
java.time.ZonedDateTime.parse("2020-01-01T00:00:00Z");
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
}
/** Helper method to load a PDF file from test resources */
private MockMultipartFile loadPdfFromResources(String filename) throws IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = getClass().getClassLoader();
}
if (classLoader != null) {
try (InputStream resourceStream = classLoader.getResourceAsStream(filename)) {
if (resourceStream != null) {
byte[] content = resourceStream.readAllBytes();
return new MockMultipartFile(
"file", filename, MediaType.APPLICATION_PDF_VALUE, content);
}
}
}
Path projectRoot = locateProjectRoot(Path.of("").toAbsolutePath());
List<Path> searchDirectories =
List.of(
projectRoot.resolve(
Path.of("app", "core", "src", "test", "resources").toString()),
projectRoot.resolve(
Path.of("app", "common", "src", "test", "resources").toString()),
projectRoot.resolve(
Path.of("testing", "cucumber", "exampleFiles").toString()));
for (Path directory : searchDirectories) {
Path filePath = directory.resolve(filename);
if (Files.exists(filePath)) {
byte[] content = Files.readAllBytes(filePath);
return new MockMultipartFile(
"file", filename, MediaType.APPLICATION_PDF_VALUE, content);
}
}
throw new IOException("PDF file not found: " + filename);
}
private Path locateProjectRoot(Path start) {
Path current = start;
while (current != null) {
if (Files.exists(current.resolve("settings.gradle"))) {
return current;
}
current = current.getParent();
}
return start;
}
/** Helper method to create a simple PDF document with text */
private PDDocument createSimplePdfWithText(String text) throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
contentStream.newLineAtOffset(100, 700);
contentStream.showText(text);
contentStream.endText();
}
return document;
}
/** Helper method to create a PDF with metadata */
private PDDocument createPdfWithMetadata() throws IOException {
PDDocument document = createSimplePdfWithText("Test document with metadata");
PDDocumentInformation info = new PDDocumentInformation();
info.setTitle("Test Title");
info.setAuthor("Test Author");
info.setSubject("Test Subject");
info.setKeywords("test, pdf, metadata");
info.setCreator("Test Creator");
info.setProducer("Test Producer");
GregorianCalendar cal = GregorianCalendar.from(FIXED_NOW);
info.setCreationDate(cal);
info.setModificationDate(cal);
document.setDocumentInformation(info);
return document;
}
/** Helper method to create an encrypted PDF */
private PDDocument createEncryptedPdf() throws IOException {
PDDocument document = createSimplePdfWithText("Encrypted content");
AccessPermission accessPermission = new AccessPermission();
accessPermission.setCanPrint(false);
accessPermission.setCanModify(false);
ProtectionPolicy protectionPolicy =
new StandardProtectionPolicy("owner", "user", accessPermission);
document.protect(protectionPolicy);
return document;
}
/** Helper method to convert PDDocument to MockMultipartFile */
private MockMultipartFile documentToMultipartFile(PDDocument document, String filename)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
return new MockMultipartFile(
"file", filename, MediaType.APPLICATION_PDF_VALUE, baos.toByteArray());
}
@Nested
@DisplayName("Basic Functionality Tests")
class BasicFunctionalityTests {
@Test
@DisplayName("Should successfully extract info from a valid PDF")
void testGetPdfInfo_ValidPdf() throws IOException {
PDDocument document = createPdfWithMetadata();
MockMultipartFile mockFile = documentToMultipartFile(document, "test.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
try (PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes())) {
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
Assertions.assertNotNull(response.getBody());
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertTrue(jsonNode.has("Metadata"));
Assertions.assertTrue(jsonNode.has("BasicInfo"));
Assertions.assertTrue(jsonNode.has("DocumentInfo"));
Assertions.assertTrue(jsonNode.has("Compliancy"));
Assertions.assertTrue(jsonNode.has("Encryption"));
Assertions.assertTrue(jsonNode.has("Permissions"));
JsonNode metadata = jsonNode.get("Metadata");
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
}
}
@Test
@DisplayName("Should extract basic info correctly")
void testGetPdfInfo_BasicInfo() throws IOException {
PDDocument document = createSimplePdfWithText("Test content with some words");
MockMultipartFile mockFile = documentToMultipartFile(document, "basic.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
try (PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes())) {
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode basicInfo = jsonNode.get("BasicInfo");
Assertions.assertTrue(basicInfo.has("Number of pages"));
Assertions.assertTrue(basicInfo.has("FileSizeInBytes"));
Assertions.assertTrue(basicInfo.has("WordCount"));
Assertions.assertTrue(basicInfo.has("CharacterCount"));
Assertions.assertEquals(1, basicInfo.get("Number of pages").asInt());
Assertions.assertTrue(basicInfo.get("FileSizeInBytes").asLong() > 0);
}
}
@Test
@DisplayName("Should handle PDF with multiple pages")
void testGetPdfInfo_MultiplePages() throws IOException {
PDDocument document = new PDDocument();
document.addPage(new PDPage(PDRectangle.A4));
document.addPage(new PDPage(PDRectangle.A4));
document.addPage(new PDPage(PDRectangle.LETTER));
MockMultipartFile mockFile = documentToMultipartFile(document, "multipage.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
try (PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes())) {
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertEquals(
3, jsonNode.get("BasicInfo").get("Number of pages").asInt());
Assertions.assertTrue(jsonNode.has("PerPageInfo"));
JsonNode perPageInfo = jsonNode.get("PerPageInfo");
Assertions.assertTrue(perPageInfo.has("Page 1"));
Assertions.assertTrue(perPageInfo.has("Page 2"));
Assertions.assertTrue(perPageInfo.has("Page 3"));
}
}
}
@Nested
@DisplayName("Metadata Extraction Tests")
class MetadataExtractionTests {
@Test
@DisplayName("Should extract all metadata fields")
void testExtractMetadata_AllFields() throws IOException {
PDDocument document = createPdfWithMetadata();
MockMultipartFile mockFile = documentToMultipartFile(document, "metadata.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode metadata = jsonNode.get("Metadata");
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText());
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText());
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText());
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText());
Assertions.assertTrue(metadata.has("CreationDate"));
Assertions.assertTrue(metadata.has("ModificationDate"));
loadedDoc.close();
}
@Test
@DisplayName("Should handle PDF with missing metadata")
void testExtractMetadata_MissingFields() throws IOException {
PDDocument document = createSimplePdfWithText("No metadata");
MockMultipartFile mockFile = documentToMultipartFile(document, "no-metadata.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode metadata = jsonNode.get("Metadata");
Assertions.assertNotNull(metadata);
loadedDoc.close();
}
}
@Nested
@DisplayName("Encryption and Permissions Tests")
class EncryptionPermissionsTests {
@Test
@DisplayName("Should detect unencrypted PDF")
void testEncryption_UnencryptedPdf() throws IOException {
PDDocument document = createSimplePdfWithText("Not encrypted");
MockMultipartFile mockFile = documentToMultipartFile(document, "unencrypted.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode encryption = jsonNode.get("Encryption");
Assertions.assertFalse(encryption.get("IsEncrypted").asBoolean());
loadedDoc.close();
}
@Test
@DisplayName("Should extract all permissions")
void testPermissions_AllPermissions() throws IOException {
PDDocument document = createSimplePdfWithText("Test permissions");
MockMultipartFile mockFile = documentToMultipartFile(document, "permissions.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode permissions = jsonNode.get("Permissions");
Assertions.assertTrue(permissions.has("Document Assembly"));
Assertions.assertTrue(permissions.has("Extracting Content"));
Assertions.assertTrue(permissions.has("Form Filling"));
Assertions.assertTrue(permissions.has("Modifying"));
Assertions.assertTrue(permissions.has("Printing"));
loadedDoc.close();
}
}
@Nested
@DisplayName("Form Fields Tests")
class FormFieldsTests {
@Test
@DisplayName("Should extract form fields section from PDF")
void testFormFields_Structure() throws IOException {
PDDocument document = createSimplePdfWithText("Document to test form fields section");
MockMultipartFile mockFile = documentToMultipartFile(document, "test-forms.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertTrue(jsonNode.has("FormFields"));
JsonNode formFields = jsonNode.get("FormFields");
Assertions.assertNotNull(formFields);
loadedDoc.close();
}
@Test
@DisplayName("Should handle PDF without form fields")
void testFormFields_NoFields() throws IOException {
PDDocument document = createSimplePdfWithText("No form fields");
MockMultipartFile mockFile = documentToMultipartFile(document, "no-forms.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode formFields = jsonNode.get("FormFields");
Assertions.assertEquals(0, formFields.size());
loadedDoc.close();
}
}
@Nested
@DisplayName("Per-Page Information Tests")
class PerPageInfoTests {
@Test
@DisplayName("Should extract page dimensions")
void testPerPageInfo_Dimensions() throws IOException {
PDDocument document = new PDDocument();
document.addPage(new PDPage(PDRectangle.A4));
document.addPage(new PDPage(PDRectangle.LETTER));
MockMultipartFile mockFile = documentToMultipartFile(document, "dimensions.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode perPageInfo = jsonNode.get("PerPageInfo");
JsonNode page1 = perPageInfo.get("Page 1");
Assertions.assertTrue(page1.has("Size"));
Assertions.assertTrue(page1.get("Size").has("Standard Page"));
Assertions.assertEquals("A4", page1.get("Size").get("Standard Page").asText());
JsonNode page2 = perPageInfo.get("Page 2");
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText());
loadedDoc.close();
}
@Test
@DisplayName("Should extract page rotation")
void testPerPageInfo_Rotation() throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
page.setRotation(90);
document.addPage(page);
MockMultipartFile mockFile = documentToMultipartFile(document, "rotated.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode page1 = jsonNode.get("PerPageInfo").get("Page 1");
Assertions.assertEquals(90, page1.get("Rotation").asInt());
loadedDoc.close();
}
}
@Nested
@DisplayName("Validation and Error Handling Tests")
class ValidationErrorTests {
@Test
@DisplayName("Should reject null file")
void testValidation_NullFile() throws IOException {
PDFFile request = new PDFFile();
request.setFileInput(null);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
Assertions.assertEquals(
HttpStatus.OK, response.getStatusCode()); // Returns error JSON with 200
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertTrue(jsonNode.has("error"));
Assertions.assertTrue(jsonNode.get("error").asText().contains("PDF file is required"));
}
@Test
@DisplayName("Should reject empty file")
void testValidation_EmptyFile() throws IOException {
MockMultipartFile emptyFile =
new MockMultipartFile(
"file", "empty.pdf", MediaType.APPLICATION_PDF_VALUE, new byte[0]);
PDFFile request = new PDFFile();
request.setFileInput(emptyFile);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertTrue(jsonNode.has("error"));
}
@Test
@DisplayName("Should reject file that exceeds max size")
void testValidation_TooLargeFile() throws IOException {
MultipartFile largeFile =
new MultipartFile() {
@Override
public String getName() {
return "file";
}
@Override
public String getOriginalFilename() {
return "large.pdf";
}
@Override
public String getContentType() {
return MediaType.APPLICATION_PDF_VALUE;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public long getSize() {
// Report 101 MB without allocating memory
return 101L * 1024L * 1024L;
}
@Override
public byte[] getBytes() {
return new byte[0];
}
@Override
public java.io.InputStream getInputStream() {
return java.io.InputStream.nullInputStream();
}
@Override
public void transferTo(java.io.File dest) throws IllegalStateException {}
};
PDFFile request = new PDFFile();
request.setFileInput(largeFile);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertTrue(jsonNode.has("error"));
Assertions.assertTrue(
jsonNode.get("error").asText().contains("exceeds maximum allowed size"));
}
}
@Nested
@DisplayName("Static Helper Methods Tests")
class HelperMethodsTests {
@Test
@DisplayName("Should determine page orientation correctly")
void testGetPageOrientation() {
Assertions.assertEquals("Landscape", GetInfoOnPDF.getPageOrientation(800, 600));
Assertions.assertEquals("Portrait", GetInfoOnPDF.getPageOrientation(600, 800));
Assertions.assertEquals("Square", GetInfoOnPDF.getPageOrientation(600, 600));
}
@ParameterizedTest
@CsvSource({
"612, 792, Letter",
"595.276, 841.89, A4",
"2383.937, 3370.394, A0",
"100, 100, Custom"
})
@DisplayName("Should identify standard page sizes")
void testGetPageSize(float width, float height, String expected) {
Assertions.assertEquals(expected, GetInfoOnPDF.getPageSize(width, height));
}
@Test
@DisplayName("Should check for PDF/A standard")
void testCheckForStandard_PdfA() throws IOException {
// This would require a real PDF/A document or mocking
PDDocument document = createSimplePdfWithText("Test");
boolean result = GetInfoOnPDF.checkForStandard(document, "PDF/A");
Assertions.assertFalse(result); // Simple PDF is not PDF/A compliant
document.close();
}
@Test
@DisplayName("Should handle null document in checkForStandard")
void testCheckForStandard_NullDocument() {
boolean result = GetInfoOnPDF.checkForStandard(null, "PDF/A");
Assertions.assertFalse(result);
}
@Test
@DisplayName("Should get PDF/A conformance level")
void testGetPdfAConformanceLevel() throws IOException {
PDDocument document = createSimplePdfWithText("Test");
String level = GetInfoOnPDF.getPdfAConformanceLevel(document);
Assertions.assertNull(level);
document.close();
}
@Test
@DisplayName("Should handle encrypted document in getPdfAConformanceLevel")
void testGetPdfAConformanceLevel_EncryptedDocument() throws IOException {
PDDocument document = createEncryptedPdf();
String level = GetInfoOnPDF.getPdfAConformanceLevel(document);
Assertions.assertNull(level); // Encrypted documents return null
document.close();
}
}
@Nested
@DisplayName("Real PDF Files Tests")
class RealPdfFilesTests {
@Test
@DisplayName("Should process example.pdf from test resources")
void testRealPdf_Example() {
try {
MockMultipartFile mockFile = loadPdfFromResources("example.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
try (PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes())) {
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertFalse(
jsonNode.has("error"), "Should not have error in response");
Assertions.assertTrue(jsonNode.has("BasicInfo"));
Assertions.assertTrue(jsonNode.has("DocumentInfo"));
Assertions.assertTrue(jsonNode.get("DocumentInfo").has("PDF version"));
}
} catch (IOException e) {
Assumptions.assumeTrue(
false, "Skipping test - example.pdf not found: " + e.getMessage());
}
}
@Test
@DisplayName("Should process tables.pdf")
void testRealPdf_Tables() {
try {
MockMultipartFile mockFile = loadPdfFromResources("tables.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
try (PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes())) {
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
Assertions.assertNotNull(response);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertFalse(jsonNode.has("error"));
Assertions.assertTrue(jsonNode.has("BasicInfo"));
}
} catch (IOException e) {
Assumptions.assumeTrue(
false, "Skipping test - tables.pdf not found: " + e.getMessage());
}
}
}
@Nested
@DisplayName("Compliance Tests")
class ComplianceTests {
@Test
@DisplayName("Should check PDF/A compliance")
void testCompliance_PdfA() throws IOException {
PDDocument document = createSimplePdfWithText("Test PDF/A");
MockMultipartFile mockFile = documentToMultipartFile(document, "pdfa.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode compliancy = jsonNode.get("Compliancy");
Assertions.assertTrue(compliancy.has("IsPDF/ACompliant"));
Assertions.assertTrue(compliancy.has("IsPDF/XCompliant"));
Assertions.assertTrue(compliancy.has("IsPDF/ECompliant"));
Assertions.assertTrue(compliancy.has("IsPDF/UACompliant"));
loadedDoc.close();
}
}
@Nested
@DisplayName("Image Statistics Tests")
class ImageStatisticsTests {
@Test
@DisplayName("Should extract image statistics from PDF")
void testImageStatistics() throws IOException {
PDDocument document = createSimplePdfWithText("Document for image statistics");
MockMultipartFile mockFile = documentToMultipartFile(document, "no-images.pdf");
PDFFile request = new PDFFile();
request.setFileInput(mockFile);
PDDocument loadedDoc = Loader.loadPDF(mockFile.getBytes());
Mockito.when(
pdfDocumentFactory.load(
ArgumentMatchers.any(MultipartFile.class),
ArgumentMatchers.anyBoolean()))
.thenReturn(loadedDoc);
ResponseEntity<byte[]> response = getInfoOnPDF.getPdfInfo(request);
String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8);
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode basicInfo = jsonNode.get("BasicInfo");
Assertions.assertTrue(basicInfo.has("TotalImages"));
Assertions.assertTrue(basicInfo.has("UniqueImages"));
Assertions.assertEquals(0, basicInfo.get("TotalImages").asInt());
Assertions.assertEquals(0, basicInfo.get("UniqueImages").asInt());
loadedDoc.close();
}
}
}
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api.security;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.awt.Color;
@@ -70,61 +69,45 @@ class RedactControllerTest {
private PDDocument realDocument;
private PDPage realPage;
// Helpers
private void testAutoRedaction(
String searchText,
boolean useRegex,
boolean wholeWordSearch,
String redactColor,
float padding,
boolean convertToImage,
boolean expectSuccess)
throws Exception {
RedactPdfRequest request = createRedactPdfRequest();
request.setListOfText(searchText);
request.setUseRegex(useRegex);
request.setWholeWordSearch(wholeWordSearch);
request.setRedactColor(redactColor);
request.setCustomPadding(padding);
request.setConvertPDFToImage(convertToImage);
try {
ResponseEntity<byte[]> response = redactController.redactPdf(request);
if (expectSuccess && response != null) {
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class));
verify(mockDocument, times(1)).close();
}
} catch (Exception e) {
if (expectSuccess) {
log.info("Redaction test completed with graceful handling: {}", e.getMessage());
} else {
assertNotNull(e.getMessage());
private static byte[] createSimplePdfContent() throws IOException {
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(PDRectangle.A4);
doc.addPage(page);
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
contentStream.newLineAtOffset(100, 700);
contentStream.showText("This is a simple PDF.");
contentStream.endText();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
return baos.toByteArray();
}
}
private void testManualRedaction(List<RedactionArea> redactionAreas, boolean convertToImage)
throws Exception {
ManualRedactPdfRequest request = createManualRedactPdfRequest();
request.setRedactions(redactionAreas);
request.setConvertPDFToImage(convertToImage);
private static List<RedactionArea> createValidRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
try {
ResponseEntity<byte[]> response = redactController.redactPDF(request);
RedactionArea area1 = new RedactionArea();
area1.setPage(1);
area1.setX(100.0);
area1.setY(100.0);
area1.setWidth(200.0);
area1.setHeight(50.0);
area1.setColor("000000");
areas.add(area1);
if (response != null) {
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class));
}
} catch (Exception e) {
log.info("Manual redaction test completed with graceful handling: {}", e.getMessage());
}
RedactionArea area2 = new RedactionArea();
area2.setPage(1);
area2.setX(300.0);
area2.setY(200.0);
area2.setWidth(150.0);
area2.setHeight(30.0);
area2.setColor("FF0000");
areas.add(area2);
return areas;
}
@BeforeEach
@@ -190,16 +173,18 @@ class RedactControllerTest {
setupRealDocument();
}
private void setupRealDocument() throws IOException {
realDocument = new PDDocument();
realPage = new PDPage(PDRectangle.A4);
realDocument.addPage(realPage);
private static List<RedactionArea> createInvalidRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
// Set up basic page resources
PDResources resources = new PDResources();
resources.put(
COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
realPage.setResources(resources);
RedactionArea invalidArea = new RedactionArea();
invalidArea.setPage(null); // Invalid - null page
invalidArea.setX(100.0);
invalidArea.setY(100.0);
invalidArea.setWidth(200.0);
invalidArea.setHeight(50.0);
areas.add(invalidArea);
return areas;
}
@AfterEach
@@ -608,13 +593,266 @@ class RedactControllerTest {
}
}
private static List<RedactionArea> createMultipleRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
for (int i = 0; i < 5; i++) {
RedactionArea area = new RedactionArea();
area.setPage(1);
area.setX(50.0 + (i * 60));
area.setY(50.0 + (i * 40));
area.setWidth(50.0);
area.setHeight(30.0);
area.setColor(String.format("%06X", i * 0x333333));
areas.add(area);
}
return areas;
}
private static List<RedactionArea> createOverlappingRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
RedactionArea area1 = new RedactionArea();
area1.setPage(1);
area1.setX(100.0);
area1.setY(100.0);
area1.setWidth(200.0);
area1.setHeight(100.0);
area1.setColor("FF0000");
areas.add(area1);
RedactionArea area2 = new RedactionArea();
area2.setPage(1);
area2.setX(150.0); // Overlaps with area1
area2.setY(150.0); // Overlaps with area1
area2.setWidth(200.0);
area2.setHeight(100.0);
area2.setColor("00FF00");
areas.add(area2);
return areas;
}
// Helper for token creation
private static List<Object> createSampleTokenList() {
return List.of(
Operator.getOperator("BT"),
COSName.getPDFName("F1"),
new COSFloat(12),
Operator.getOperator("Tf"),
new COSString("Sample text"),
Operator.getOperator("Tj"),
Operator.getOperator("ET"));
}
private RedactPdfRequest createRedactPdfRequest() {
RedactPdfRequest request = new RedactPdfRequest();
request.setFileInput(mockPdfFile);
return request;
}
private ManualRedactPdfRequest createManualRedactPdfRequest() {
ManualRedactPdfRequest request = new ManualRedactPdfRequest();
request.setFileInput(mockPdfFile);
return request;
}
private static String extractTextFromTokens(List<Object> tokens) {
StringBuilder text = new StringBuilder();
for (Object token : tokens) {
if (token instanceof COSString cosString) {
text.append(cosString.getString());
} else if (token instanceof COSArray array) {
for (int i = 0; i < array.size(); i++) {
if (array.getObject(i) instanceof COSString cosString) {
text.append(cosString.getString());
}
}
}
}
return text.toString();
}
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
return buffer.toByteArray();
}
// Helpers
private void testAutoRedaction(
String searchText,
boolean useRegex,
boolean wholeWordSearch,
String redactColor,
float padding,
boolean convertToImage,
boolean expectSuccess) {
RedactPdfRequest request = createRedactPdfRequest();
request.setListOfText(searchText);
request.setUseRegex(useRegex);
request.setWholeWordSearch(wholeWordSearch);
request.setRedactColor(redactColor);
request.setCustomPadding(padding);
request.setConvertPDFToImage(convertToImage);
try {
ResponseEntity<byte[]> response = redactController.redactPdf(request);
if (expectSuccess && response != null) {
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class));
verify(mockDocument, times(1)).close();
}
} catch (Exception e) {
if (expectSuccess) {
log.info("Redaction test completed with graceful handling: {}", e.getMessage());
} else {
assertNotNull(e.getMessage());
}
}
}
private void testManualRedaction(List<RedactionArea> redactionAreas, boolean convertToImage) {
ManualRedactPdfRequest request = createManualRedactPdfRequest();
request.setRedactions(redactionAreas);
request.setConvertPDFToImage(convertToImage);
try {
ResponseEntity<byte[]> response = redactController.redactPDF(request);
if (response != null) {
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class));
}
} catch (Exception e) {
log.info("Manual redaction test completed with graceful handling: {}", e.getMessage());
}
}
private void setupRealDocument() {
realDocument = new PDDocument();
realPage = new PDPage(PDRectangle.A4);
realDocument.addPage(realPage);
// Set up basic page resources
PDResources resources = new PDResources();
resources.put(
COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
realPage.setResources(resources);
}
// Helper methods for real PDF content creation
private void createRealPageWithSimpleText(String text) throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText(text);
contentStream.endText();
}
}
private void createRealPageWithTJArrayText() throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("This is ");
contentStream.newLineAtOffset(-10, 0); // Simulate positioning
contentStream.showText("secret");
contentStream.newLineAtOffset(10, 0); // Reset positioning
contentStream.showText(" information");
contentStream.endText();
}
}
private void createRealPageWithMixedContent() throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.setLineWidth(2);
contentStream.moveTo(100, 100);
contentStream.lineTo(200, 200);
contentStream.stroke();
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("Please redact this content");
contentStream.endText();
}
}
private void createRealPageWithSpecificOperator(String operatorName) throws IOException {
createRealPageWithSimpleText("sensitive data");
}
private void createRealPageWithPositionedText() throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("Normal text ");
contentStream.newLineAtOffset(100, 0);
contentStream.showText("confidential");
contentStream.newLineAtOffset(100, 0);
contentStream.showText(" more text");
contentStream.endText();
}
}
@Nested
@DisplayName("Error Handling and Edge Cases")
class ErrorHandlingTests {
@Test
@DisplayName("Should handle null file input gracefully")
void handleNullFileInput() throws Exception {
void handleNullFileInput() {
RedactPdfRequest request = new RedactPdfRequest();
request.setFileInput(null);
request.setListOfText("test");
@@ -631,7 +869,7 @@ class RedactControllerTest {
@Test
@DisplayName("Should handle malformed PDF gracefully")
void handleMalformedPdfGracefully() throws Exception {
void handleMalformedPdfGracefully() {
MockMultipartFile malformedFile =
new MockMultipartFile(
"fileInput",
@@ -675,7 +913,7 @@ class RedactControllerTest {
@Test
@DisplayName("Should handle null redact color gracefully")
void handleNullRedactColor() throws Exception {
void handleNullRedactColor() {
RedactPdfRequest request = createRedactPdfRequest();
request.setListOfText("test");
request.setRedactColor(null);
@@ -723,34 +961,50 @@ class RedactControllerTest {
}
}
private List<Object> getOriginalTokens() throws Exception {
// Create a new page to avoid side effects from other tests
PDPage pageForTokenExtraction = new PDPage(PDRectangle.A4);
pageForTokenExtraction.setResources(realPage.getResources());
try (PDPageContentStream contentStream =
new PDPageContentStream(realDocument, pageForTokenExtraction)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("Original content");
contentStream.endText();
}
return redactController.createTokensWithoutTargetText(
realDocument, pageForTokenExtraction, Collections.emptySet(), false, false);
}
@Nested
@DisplayName("Color Decoding Utility Tests")
class ColorDecodingTests {
@Test
@DisplayName("Should decode valid hex color with hash")
void decodeValidHexColorWithHash() throws Exception {
void decodeValidHexColorWithHash() {
Color result = redactController.decodeOrDefault("#FF0000");
assertEquals(Color.RED, result);
}
@Test
@DisplayName("Should decode valid hex color without hash")
void decodeValidHexColorWithoutHash() throws Exception {
void decodeValidHexColorWithoutHash() {
Color result = redactController.decodeOrDefault("FF0000");
assertEquals(Color.RED, result);
}
@Test
@DisplayName("Should default to black for null color")
void defaultToBlackForNullColor() throws Exception {
void defaultToBlackForNullColor() {
Color result = redactController.decodeOrDefault(null);
assertEquals(Color.BLACK, result);
}
@Test
@DisplayName("Should default to black for invalid color")
void defaultToBlackForInvalidColor() throws Exception {
void defaultToBlackForInvalidColor() {
Color result = redactController.decodeOrDefault("invalid-color");
assertEquals(Color.BLACK, result);
}
@@ -762,7 +1016,7 @@ class RedactControllerTest {
"0000FF"
})
@DisplayName("Should handle various valid color formats")
void handleVariousValidColorFormats(String colorInput) throws Exception {
void handleVariousValidColorFormats(String colorInput) {
Color result = redactController.decodeOrDefault(colorInput);
assertNotNull(result);
assertTrue(
@@ -778,7 +1032,7 @@ class RedactControllerTest {
@Test
@DisplayName("Should handle short hex codes appropriately")
void handleShortHexCodes() throws Exception {
void handleShortHexCodes() {
Color result1 = redactController.decodeOrDefault("123");
Color result2 = redactController.decodeOrDefault("#12");
@@ -787,6 +1041,15 @@ class RedactControllerTest {
}
}
private String extractTextFromModifiedPage(PDPage page) throws IOException {
if (page.getContents() != null) {
try (InputStream inputStream = page.getContents()) {
return new String(readAllBytes(inputStream));
}
}
return "";
}
@Nested
@DisplayName("Content Stream Unit Tests")
class ContentStreamUnitTests {
@@ -975,7 +1238,7 @@ class RedactControllerTest {
@Test
@DisplayName("Placeholder creation should maintain text width")
void shouldCreateWidthMatchingPlaceholder() throws Exception {
void shouldCreateWidthMatchingPlaceholder() {
String originalText = "confidential";
String placeholder =
redactController.createPlaceholderWithFont(
@@ -989,7 +1252,7 @@ class RedactControllerTest {
@Test
@DisplayName("Placeholder should handle special characters")
void shouldHandleSpecialCharactersInPlaceholder() throws Exception {
void shouldHandleSpecialCharactersInPlaceholder() {
String originalText = "café naïve";
String placeholder =
redactController.createPlaceholderWithFont(
@@ -1164,270 +1427,4 @@ class RedactControllerTest {
assertTrue(response.getBody().length > 0);
}
}
private RedactPdfRequest createRedactPdfRequest() {
RedactPdfRequest request = new RedactPdfRequest();
request.setFileInput(mockPdfFile);
return request;
}
private ManualRedactPdfRequest createManualRedactPdfRequest() {
ManualRedactPdfRequest request = new ManualRedactPdfRequest();
request.setFileInput(mockPdfFile);
return request;
}
private byte[] createSimplePdfContent() throws IOException {
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(PDRectangle.A4);
doc.addPage(page);
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
contentStream.newLineAtOffset(100, 700);
contentStream.showText("This is a simple PDF.");
contentStream.endText();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
return baos.toByteArray();
}
}
private List<RedactionArea> createValidRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
RedactionArea area1 = new RedactionArea();
area1.setPage(1);
area1.setX(100.0);
area1.setY(100.0);
area1.setWidth(200.0);
area1.setHeight(50.0);
area1.setColor("000000");
areas.add(area1);
RedactionArea area2 = new RedactionArea();
area2.setPage(1);
area2.setX(300.0);
area2.setY(200.0);
area2.setWidth(150.0);
area2.setHeight(30.0);
area2.setColor("FF0000");
areas.add(area2);
return areas;
}
private List<RedactionArea> createInvalidRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
RedactionArea invalidArea = new RedactionArea();
invalidArea.setPage(null); // Invalid - null page
invalidArea.setX(100.0);
invalidArea.setY(100.0);
invalidArea.setWidth(200.0);
invalidArea.setHeight(50.0);
areas.add(invalidArea);
return areas;
}
private List<RedactionArea> createMultipleRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
for (int i = 0; i < 5; i++) {
RedactionArea area = new RedactionArea();
area.setPage(1);
area.setX(50.0 + (i * 60));
area.setY(50.0 + (i * 40));
area.setWidth(50.0);
area.setHeight(30.0);
area.setColor(String.format("%06X", i * 0x333333));
areas.add(area);
}
return areas;
}
private List<RedactionArea> createOverlappingRedactionAreas() {
List<RedactionArea> areas = new ArrayList<>();
RedactionArea area1 = new RedactionArea();
area1.setPage(1);
area1.setX(100.0);
area1.setY(100.0);
area1.setWidth(200.0);
area1.setHeight(100.0);
area1.setColor("FF0000");
areas.add(area1);
RedactionArea area2 = new RedactionArea();
area2.setPage(1);
area2.setX(150.0); // Overlaps with area1
area2.setY(150.0); // Overlaps with area1
area2.setWidth(200.0);
area2.setHeight(100.0);
area2.setColor("00FF00");
areas.add(area2);
return areas;
}
// Helper methods for real PDF content creation
private void createRealPageWithSimpleText(String text) throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText(text);
contentStream.endText();
}
}
private void createRealPageWithTJArrayText() throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("This is ");
contentStream.newLineAtOffset(-10, 0); // Simulate positioning
contentStream.showText("secret");
contentStream.newLineAtOffset(10, 0); // Reset positioning
contentStream.showText(" information");
contentStream.endText();
}
}
private void createRealPageWithMixedContent() throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.setLineWidth(2);
contentStream.moveTo(100, 100);
contentStream.lineTo(200, 200);
contentStream.stroke();
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("Please redact this content");
contentStream.endText();
}
}
private void createRealPageWithSpecificOperator(String operatorName) throws IOException {
createRealPageWithSimpleText("sensitive data");
}
private void createRealPageWithPositionedText() throws IOException {
realPage = new PDPage(PDRectangle.A4);
while (realDocument.getNumberOfPages() > 0) {
realDocument.removePage(0);
}
realDocument.addPage(realPage);
realPage.setResources(new PDResources());
realPage.getResources()
.put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA));
try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("Normal text ");
contentStream.newLineAtOffset(100, 0);
contentStream.showText("confidential");
contentStream.newLineAtOffset(100, 0);
contentStream.showText(" more text");
contentStream.endText();
}
}
// Helper for token creation
private List<Object> createSampleTokenList() {
return List.of(
Operator.getOperator("BT"),
COSName.getPDFName("F1"),
new COSFloat(12),
Operator.getOperator("Tf"),
new COSString("Sample text"),
Operator.getOperator("Tj"),
Operator.getOperator("ET"));
}
private List<Object> getOriginalTokens() throws Exception {
// Create a new page to avoid side effects from other tests
PDPage pageForTokenExtraction = new PDPage(PDRectangle.A4);
pageForTokenExtraction.setResources(realPage.getResources());
try (PDPageContentStream contentStream =
new PDPageContentStream(realDocument, pageForTokenExtraction)) {
contentStream.beginText();
contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12);
contentStream.newLineAtOffset(50, 750);
contentStream.showText("Original content");
contentStream.endText();
}
return redactController.createTokensWithoutTargetText(
realDocument, pageForTokenExtraction, Collections.emptySet(), false, false);
}
private String extractTextFromTokens(List<Object> tokens) {
StringBuilder text = new StringBuilder();
for (Object token : tokens) {
if (token instanceof COSString cosString) {
text.append(cosString.getString());
} else if (token instanceof COSArray array) {
for (int i = 0; i < array.size(); i++) {
if (array.getObject(i) instanceof COSString cosString) {
text.append(cosString.getString());
}
}
}
}
return text.toString();
}
private String extractTextFromModifiedPage(PDPage page) throws IOException {
if (page.getContents() != null) {
try (InputStream inputStream = page.getContents()) {
return new String(readAllBytes(inputStream));
}
}
return "";
}
private byte[] readAllBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
return buffer.toByteArray();
}
}
@@ -0,0 +1,231 @@
package stirling.software.SPDF.controller.web;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.CheckProgramInstall;
@ExtendWith(MockitoExtension.class)
class ConverterWebControllerTest {
private MockMvc mockMvc;
private ConverterWebController controller;
@BeforeEach
void setup() {
controller = new ConverterWebController();
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
private static Stream<Object[]> simpleEndpoints() {
return Stream.of(
new Object[] {"/img-to-pdf", "convert/img-to-pdf", "img-to-pdf"},
new Object[] {"/cbz-to-pdf", "convert/cbz-to-pdf", "cbz-to-pdf"},
new Object[] {"/pdf-to-cbz", "convert/pdf-to-cbz", "pdf-to-cbz"},
new Object[] {"/cbr-to-pdf", "convert/cbr-to-pdf", "cbr-to-pdf"},
new Object[] {"/html-to-pdf", "convert/html-to-pdf", "html-to-pdf"},
new Object[] {"/markdown-to-pdf", "convert/markdown-to-pdf", "markdown-to-pdf"},
new Object[] {"/pdf-to-markdown", "convert/pdf-to-markdown", "pdf-to-markdown"},
new Object[] {"/url-to-pdf", "convert/url-to-pdf", "url-to-pdf"},
new Object[] {"/file-to-pdf", "convert/file-to-pdf", "file-to-pdf"},
new Object[] {"/pdf-to-pdfa", "convert/pdf-to-pdfa", "pdf-to-pdfa"},
new Object[] {"/pdf-to-vector", "convert/pdf-to-vector", "pdf-to-vector"},
new Object[] {"/vector-to-pdf", "convert/vector-to-pdf", "vector-to-pdf"},
new Object[] {"/pdf-to-xml", "convert/pdf-to-xml", "pdf-to-xml"},
new Object[] {"/pdf-to-csv", "convert/pdf-to-csv", "pdf-to-csv"},
new Object[] {"/pdf-to-html", "convert/pdf-to-html", "pdf-to-html"},
new Object[] {
"/pdf-to-presentation", "convert/pdf-to-presentation", "pdf-to-presentation"
},
new Object[] {"/pdf-to-text", "convert/pdf-to-text", "pdf-to-text"},
new Object[] {"/pdf-to-word", "convert/pdf-to-word", "pdf-to-word"},
new Object[] {"/eml-to-pdf", "convert/eml-to-pdf", "eml-to-pdf"});
}
@ParameterizedTest(name = "[{index}] GET {0}")
@MethodSource("simpleEndpoints")
@DisplayName("Should return correct view and model for simple endpoints")
void shouldReturnCorrectViewForSimpleEndpoints(String path, String viewName, String page)
throws Exception {
mockMvc.perform(get(path))
.andExpect(status().isOk())
.andExpect(view().name(viewName))
.andExpect(model().attribute("currentPage", page));
}
@Nested
@DisplayName("PDF to CBR endpoint tests")
class PdfToCbrTests {
@Test
@DisplayName("Should return 404 when endpoint disabled")
void shouldReturn404WhenDisabled() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
when(endpointConfig.isEndpointEnabled(eq("pdf-to-cbr"))).thenReturn(false);
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
.thenReturn(endpointConfig);
mockMvc.perform(get("/pdf-to-cbr")).andExpect(status().isNotFound());
}
}
@Test
@DisplayName("Should return OK when endpoint enabled")
void shouldReturnOkWhenEnabled() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
when(endpointConfig.isEndpointEnabled(eq("pdf-to-cbr"))).thenReturn(true);
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
.thenReturn(endpointConfig);
mockMvc.perform(get("/pdf-to-cbr"))
.andExpect(status().isOk())
.andExpect(view().name("convert/pdf-to-cbr"))
.andExpect(model().attribute("currentPage", "pdf-to-cbr"));
}
}
}
@Nested
@DisplayName("PDF to EPUB endpoint tests")
class PdfToEpubTests {
@Test
@DisplayName("Should return 404 when endpoint disabled")
void shouldReturn404WhenDisabled() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
when(endpointConfig.isEndpointEnabled(eq("pdf-to-epub"))).thenReturn(false);
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
.thenReturn(endpointConfig);
mockMvc.perform(get("/pdf-to-epub")).andExpect(status().isNotFound());
}
}
@Test
@DisplayName("Should return OK when endpoint enabled")
void shouldReturnOkWhenEnabled() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
when(endpointConfig.isEndpointEnabled(eq("pdf-to-epub"))).thenReturn(true);
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
.thenReturn(endpointConfig);
mockMvc.perform(get("/pdf-to-epub"))
.andExpect(status().isOk())
.andExpect(view().name("convert/pdf-to-epub"))
.andExpect(model().attribute("currentPage", "pdf-to-epub"));
}
}
}
@Test
@DisplayName("Should handle pdf-to-img with default maxDPI=500")
void shouldHandlePdfToImgWithDefaultMaxDpi() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class);
MockedStatic<CheckProgramInstall> cpi =
org.mockito.Mockito.mockStatic(CheckProgramInstall.class)) {
cpi.when(CheckProgramInstall::isPythonAvailable).thenReturn(true);
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
.thenReturn(null);
mockMvc.perform(get("/pdf-to-img"))
.andExpect(status().isOk())
.andExpect(view().name("convert/pdf-to-img"))
.andExpect(model().attribute("isPython", true))
.andExpect(model().attribute("maxDPI", 500));
}
}
@Test
@DisplayName("Should handle pdf-to-video with default maxDPI=500")
void shouldHandlePdfToVideoWithDefaultMaxDpi() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
.thenReturn(null);
mockMvc.perform(get("/pdf-to-video"))
.andExpect(status().isOk())
.andExpect(view().name("convert/pdf-to-video"))
.andExpect(model().attribute("maxDPI", 500))
.andExpect(model().attribute("currentPage", "pdf-to-video"));
}
}
@Test
@DisplayName("Should handle pdf-to-img with configured maxDPI from properties")
void shouldHandlePdfToImgWithConfiguredMaxDpi() throws Exception {
// Covers the 'if' branch (properties and system not null)
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class);
MockedStatic<CheckProgramInstall> cpi =
org.mockito.Mockito.mockStatic(CheckProgramInstall.class)) {
ApplicationProperties properties =
org.mockito.Mockito.mock(
ApplicationProperties.class, org.mockito.Mockito.RETURNS_DEEP_STUBS);
when(properties.getSystem().getMaxDPI()).thenReturn(777);
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
.thenReturn(properties);
cpi.when(CheckProgramInstall::isPythonAvailable).thenReturn(true);
mockMvc.perform(get("/pdf-to-img"))
.andExpect(status().isOk())
.andExpect(view().name("convert/pdf-to-img"))
.andExpect(model().attribute("isPython", true))
.andExpect(model().attribute("maxDPI", 777))
.andExpect(model().attribute("currentPage", "pdf-to-img"));
}
}
@Test
@DisplayName("Should handle pdf-to-video with configured maxDPI from properties")
void shouldHandlePdfToVideoWithConfiguredMaxDpi() throws Exception {
// Covers the 'if' branch (properties and system not null)
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
ApplicationProperties properties =
org.mockito.Mockito.mock(
ApplicationProperties.class, org.mockito.Mockito.RETURNS_DEEP_STUBS);
when(properties.getSystem().getMaxDPI()).thenReturn(640);
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
.thenReturn(properties);
mockMvc.perform(get("/pdf-to-video"))
.andExpect(status().isOk())
.andExpect(view().name("convert/pdf-to-video"))
.andExpect(model().attribute("maxDPI", 640))
.andExpect(model().attribute("currentPage", "pdf-to-video"));
}
}
}
@@ -16,35 +16,8 @@ import stirling.software.common.model.ApplicationProperties;
class UploadLimitServiceTest {
private UploadLimitService uploadLimitService;
private ApplicationProperties applicationProperties;
private ApplicationProperties.System systemProps;
@BeforeEach
void setUp() {
applicationProperties = mock(ApplicationProperties.class);
systemProps = mock(ApplicationProperties.System.class);
when(applicationProperties.getSystem()).thenReturn(systemProps);
uploadLimitService = new UploadLimitService();
// inject mock
try {
var field = UploadLimitService.class.getDeclaredField("applicationProperties");
field.setAccessible(true);
field.set(uploadLimitService, applicationProperties);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
@ParameterizedTest(name = "getUploadLimit case #{index}: input={0}, expected={1}")
@MethodSource("uploadLimitParams")
void shouldComputeUploadLimitCorrectly(String input, long expected) {
when(systemProps.getFileUploadLimit()).thenReturn(input);
long result = uploadLimitService.getUploadLimit();
assertEquals(expected, result);
}
static Stream<Arguments> uploadLimitParams() {
return Stream.of(
// empty or null input yields 0
@@ -56,11 +29,37 @@ class UploadLimitServiceTest {
// valid formats
Arguments.of("10KB", 10 * 1024L),
Arguments.of("2MB", 2 * 1024 * 1024L),
Arguments.of("1GB", 1L * 1024 * 1024 * 1024),
Arguments.of("1GB", (long) 1024 * 1024 * 1024),
Arguments.of("5mb", 5 * 1024 * 1024L),
Arguments.of("0MB", 0L));
}
@ParameterizedTest(name = "getUploadLimit case #{index}: input={0}, expected={1}")
@MethodSource("uploadLimitParams")
void shouldComputeUploadLimitCorrectly(String input, long expected) {
when(systemProps.getFileUploadLimit()).thenReturn(input);
long result = uploadLimitService.getUploadLimit();
assertEquals(expected, result);
}
@BeforeEach
void setUp() {
ApplicationProperties applicationProperties = mock(ApplicationProperties.class);
systemProps = mock(ApplicationProperties.System.class);
when(applicationProperties.getSystem()).thenReturn(systemProps);
uploadLimitService = new UploadLimitService();
// inject mock
try {
var field = UploadLimitService.class.getDeclaredField("applicationProperties");
field.setAccessible(true);
field.set(uploadLimitService, applicationProperties);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
@ParameterizedTest(name = "getReadableUploadLimit case #{index}: rawValue={0}, expected={1}")
@MethodSource("readableLimitParams")
void shouldReturnReadableFormat(String rawValue, String expected) {
@@ -0,0 +1,104 @@
package stirling.software.SPDF.model;
import static org.junit.jupiter.api.Assertions.*;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
class ApiEndpointTest {
private final ObjectMapper mapper = new ObjectMapper();
private JsonNode postNodeWithParams(String description, String... names) {
ObjectNode post = mapper.createObjectNode();
post.put("description", description);
ArrayNode params = mapper.createArrayNode();
for (String n : names) {
ObjectNode p = mapper.createObjectNode();
if (n != null) {
p.put("name", n);
}
params.add(p);
}
post.set("parameters", params);
return post;
}
@Test
void parses_description_and_validates_required_parameters() {
JsonNode post = postNodeWithParams("Convert PDF to Markdown", "file", "mode");
ApiEndpoint endpoint = new ApiEndpoint("pdfToMd", post);
assertEquals("Convert PDF to Markdown", endpoint.getDescription());
Map<String, Object> provided = new HashMap<>();
provided.put("file", new byte[] {1});
provided.put("mode", "fast");
assertTrue(
endpoint.areParametersValid(provided), "All required keys present should be valid");
}
@Test
void missing_any_required_parameter_returns_false() {
JsonNode post = postNodeWithParams("desc", "file", "mode");
ApiEndpoint endpoint = new ApiEndpoint("pdfToMd", post);
Map<String, Object> provided = new HashMap<>();
provided.put("file", new byte[] {1});
assertFalse(endpoint.areParametersValid(provided));
}
@Test
void extra_parameters_are_ignored_if_required_are_present() {
JsonNode post = postNodeWithParams("desc", "file");
ApiEndpoint endpoint = new ApiEndpoint("x", post);
Map<String, Object> provided = new HashMap<>();
provided.put("file", new byte[] {1});
provided.put("extra", 123);
assertTrue(endpoint.areParametersValid(provided));
}
@Test
void no_parameters_defined_accepts_empty_input() {
JsonNode postEmptyArray = postNodeWithParams("desc" /* no names */);
ApiEndpoint endpointA = new ApiEndpoint("a", postEmptyArray);
assertTrue(endpointA.areParametersValid(Map.of()));
ObjectNode postNoField = mapper.createObjectNode();
postNoField.put("description", "desc");
ApiEndpoint endpointB = new ApiEndpoint("b", postNoField);
assertTrue(endpointB.areParametersValid(Map.of()));
}
@Test
void parameter_without_name_creates_empty_required_key() {
JsonNode post = postNodeWithParams("desc", (String) null);
ApiEndpoint endpoint = new ApiEndpoint("y", post);
assertFalse(endpoint.areParametersValid(Map.of()));
assertTrue(endpoint.areParametersValid(Map.of("", 42)));
}
@Test
void toString_contains_name_and_parameter_names() {
JsonNode post = postNodeWithParams("desc", "file", "mode");
ApiEndpoint endpoint = new ApiEndpoint("pdfToMd", post);
String s = endpoint.toString();
assertTrue(s.contains("pdfToMd"));
assertTrue(s.contains("file"));
assertTrue(s.contains("mode"));
}
}
@@ -0,0 +1,54 @@
package stirling.software.SPDF.model;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
class SortTypesTest {
private static final Set<String> EXPECTED =
Set.of(
"CUSTOM",
"REVERSE_ORDER",
"DUPLEX_SORT",
"BOOKLET_SORT",
"SIDE_STITCH_BOOKLET_SORT",
"ODD_EVEN_SPLIT",
"ODD_EVEN_MERGE",
"REMOVE_FIRST",
"REMOVE_LAST",
"REMOVE_FIRST_AND_LAST",
"DUPLICATE");
@Test
void contains_exactly_expected_constants() {
Set<String> actual =
Arrays.stream(SortTypes.values()).map(Enum::name).collect(Collectors.toSet());
assertEquals(
EXPECTED,
actual,
() -> "Enum constants mismatch.\nExpected: " + EXPECTED + "\nActual: " + actual);
}
@ParameterizedTest
@EnumSource(SortTypes.class)
void valueOf_roundtrip(SortTypes type) {
assertEquals(type, SortTypes.valueOf(type.name()));
}
@Test
void names_are_unique_and_uppercase() {
String[] names = Arrays.stream(SortTypes.values()).map(Enum::name).toArray(String[]::new);
assertEquals(names.length, Set.of(names).size(), "Duplicate enum names?");
for (String n : names) {
assertEquals(n, n.toUpperCase(), "Enum name not uppercase: " + n);
}
}
}
@@ -0,0 +1,84 @@
package stirling.software.SPDF.model.api;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class PDFWithPageNumsTest {
private PDFWithPageNums pdfWithPageNums;
private PDDocument mockDocument;
@BeforeEach
void setUp() {
pdfWithPageNums = new PDFWithPageNums();
mockDocument = mock(PDDocument.class);
}
@Test
void testGetPageNumbersList_AllPages() {
pdfWithPageNums.setPageNumbers("all");
when(mockDocument.getNumberOfPages()).thenReturn(10);
List<Integer> result = pdfWithPageNums.getPageNumbersList(mockDocument, true);
assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), result);
}
@Test
void testGetPageNumbersList_135_7Pages() {
pdfWithPageNums.setPageNumbers("1,3,5-7");
when(mockDocument.getNumberOfPages()).thenReturn(10);
List<Integer> result = pdfWithPageNums.getPageNumbersList(mockDocument, true);
assertEquals(List.of(1, 3, 5, 6, 7), result);
}
@Test
void testGetPageNumbersList_2nPlus1Pages() {
pdfWithPageNums.setPageNumbers("2n+1");
when(mockDocument.getNumberOfPages()).thenReturn(10);
List<Integer> result = pdfWithPageNums.getPageNumbersList(mockDocument, true);
assertEquals(List.of(3, 5, 7, 9), result);
}
@Test
void testGetPageNumbersList_3nPages() {
pdfWithPageNums.setPageNumbers("3n");
when(mockDocument.getNumberOfPages()).thenReturn(10);
List<Integer> result = pdfWithPageNums.getPageNumbersList(mockDocument, true);
assertEquals(List.of(3, 6, 9), result);
}
@Test
void testGetPageNumbersList_EmptyInput() {
pdfWithPageNums.setPageNumbers("");
when(mockDocument.getNumberOfPages()).thenReturn(10);
List<Integer> result = pdfWithPageNums.getPageNumbersList(mockDocument, true);
assertTrue(result.isEmpty());
}
@Test
void testGetPageNumbersList_InvalidInput() {
pdfWithPageNums.setPageNumbers("invalid");
when(mockDocument.getNumberOfPages()).thenReturn(10);
assertThrows(
IllegalArgumentException.class,
() -> {
pdfWithPageNums.getPageNumbersList(mockDocument, true);
});
}
}
@@ -0,0 +1,109 @@
package stirling.software.SPDF.model.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.util.PDFToFile;
class ConvertPDFToMarkdownTest {
private MockMvc mockMvc() {
return MockMvcBuilders.standaloneSetup(new ConvertPDFToMarkdown(null))
.setControllerAdvice(new GlobalErrorHandler())
.build();
}
@RestControllerAdvice
static class GlobalErrorHandler {
@ExceptionHandler(Exception.class)
ResponseEntity<byte[]> handle(Exception ex) {
String message = ex.getMessage();
byte[] body = message != null ? message.getBytes(StandardCharsets.UTF_8) : new byte[0];
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
@Test
void pdfToMarkdownReturnsMarkdownBytes() throws Exception {
byte[] md = "# heading\n\ncontent\n".getBytes(StandardCharsets.UTF_8);
try (MockedConstruction<PDFToFile> construction =
Mockito.mockConstruction(
PDFToFile.class,
(mock, ctx) -> {
when(mock.processPdfToMarkdown(any(MultipartFile.class)))
.thenAnswer(
inv ->
ResponseEntity.ok()
.header("Content-Type", "text/markdown")
.body(md));
})) {
MockMvc mvc = mockMvc();
MockMultipartFile file =
new MockMultipartFile(
"fileInput", // must match the field name in PDFFile
"input.pdf",
"application/pdf",
new byte[] {1, 2, 3});
mvc.perform(multipart("/api/v1/convert/pdf/markdown").file(file))
.andExpect(status().isOk())
.andExpect(header().string("Content-Type", "text/markdown"))
.andExpect(content().bytes(md));
// Verify that exactly one instance was created
assert construction.constructed().size() == 1;
// And that the uploaded file was passed to processPdfToMarkdown()
PDFToFile created = construction.constructed().get(0);
ArgumentCaptor<MultipartFile> captor = ArgumentCaptor.forClass(MultipartFile.class);
verify(created, times(1)).processPdfToMarkdown(captor.capture());
MultipartFile passed = captor.getValue();
// Minimal plausibility checks
assertEquals("input.pdf", passed.getOriginalFilename());
assertEquals("application/pdf", passed.getContentType());
}
}
@Test
void pdfToMarkdownWhenServiceThrowsReturns500() throws Exception {
try (MockedConstruction<PDFToFile> ignored =
Mockito.mockConstruction(
PDFToFile.class,
(mock, ctx) -> {
when(mock.processPdfToMarkdown(any(MultipartFile.class)))
.thenThrow(new RuntimeException("boom"));
})) {
MockMvc mvc = mockMvc();
MockMultipartFile file =
new MockMultipartFile(
"fileInput", "x.pdf", "application/pdf", new byte[] {0x01});
mvc.perform(multipart("/api/v1/convert/pdf/markdown").file(file))
.andExpect(status().isInternalServerError());
}
}
}
@@ -0,0 +1,130 @@
package stirling.software.SPDF.model.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
class ScannerEffectRequestTest {
private static Validator validator;
@BeforeAll
static void setupValidator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
@DisplayName("fileInput is @NotNull -> violation when missing")
void fileInput_missing_triggersViolation() {
ScannerEffectRequest req = new ScannerEffectRequest();
Set<ConstraintViolation<ScannerEffectRequest>> violations = validator.validate(req);
boolean hasFileInputViolation =
violations.stream()
.anyMatch(v -> "fileInput".contentEquals(v.getPropertyPath().toString()));
assertTrue(
hasFileInputViolation,
() ->
"Expected a validation violation on 'fileInput', but got: "
+ violations.stream()
.map(v -> v.getPropertyPath() + " -> " + v.getMessage())
.collect(Collectors.joining(", ")));
}
@Test
@DisplayName("fileInput present -> no violation for fileInput")
void fileInput_present_noViolationForThatField() {
ScannerEffectRequest req = new ScannerEffectRequest();
req.setFileInput(
new MockMultipartFile(
"fileInput", "test.pdf", "application/pdf", new byte[] {1, 2, 3}));
Set<ConstraintViolation<ScannerEffectRequest>> violations = validator.validate(req);
boolean hasFileInputViolation =
violations.stream()
.anyMatch(v -> "fileInput".contentEquals(v.getPropertyPath().toString()));
assertFalse(
hasFileInputViolation,
() ->
"Did not expect a validation violation on 'fileInput', but got: "
+ violations.stream()
.map(v -> v.getPropertyPath() + " -> " + v.getMessage())
.collect(Collectors.joining(", ")));
}
@Test
@DisplayName("applyHighQualityPreset sets documented values")
void preset_highQuality() {
ScannerEffectRequest req = new ScannerEffectRequest();
req.applyHighQualityPreset();
assertEquals(0.1f, req.getBlur(), 0.0001f);
assertEquals(1.0f, req.getNoise(), 0.0001f);
assertEquals(1.03f, req.getBrightness(), 0.0001f);
assertEquals(1.06f, req.getContrast(), 0.0001f);
assertEquals(150, req.getResolution());
}
@Test
@DisplayName("applyMediumQualityPreset sets documented values")
void preset_mediumQuality() {
ScannerEffectRequest req = new ScannerEffectRequest();
req.applyMediumQualityPreset();
assertEquals(0.1f, req.getBlur(), 0.0001f);
assertEquals(1.0f, req.getNoise(), 0.0001f);
assertEquals(1.06f, req.getBrightness(), 0.0001f);
assertEquals(1.12f, req.getContrast(), 0.0001f);
assertEquals(100, req.getResolution());
}
@Test
@DisplayName("applyLowQualityPreset sets documented values")
void preset_lowQuality() {
ScannerEffectRequest req = new ScannerEffectRequest();
req.applyLowQualityPreset();
assertEquals(0.9f, req.getBlur(), 0.0001f);
assertEquals(2.5f, req.getNoise(), 0.0001f);
assertEquals(1.08f, req.getBrightness(), 0.0001f);
assertEquals(1.15f, req.getContrast(), 0.0001f);
assertEquals(75, req.getResolution());
}
@Test
@DisplayName("getRotationValue() maps enum values to expected degrees")
void rotationValue_mapping() {
ScannerEffectRequest req = new ScannerEffectRequest();
// none -> 0
req.setRotation(ScannerEffectRequest.Rotation.none);
assertEquals(0, req.getRotationValue(), "Rotation 'none' should map to 0°");
// slight -> 2
req.setRotation(ScannerEffectRequest.Rotation.slight);
assertEquals(2, req.getRotationValue(), "Rotation 'slight' should map to 2°");
// moderate -> 5
req.setRotation(ScannerEffectRequest.Rotation.moderate);
assertEquals(5, req.getRotationValue(), "Rotation 'moderate' should map to 5°");
// severe -> 8
req.setRotation(ScannerEffectRequest.Rotation.severe);
assertEquals(8, req.getRotationValue(), "Rotation 'severe' should map to 8°");
}
}
@@ -0,0 +1,25 @@
package stirling.software.SPDF.pdf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.apache.commons.csv.CSVFormat;
import org.junit.jupiter.api.Test;
class FlexibleCSVWriterTest {
@Test
void testDefaultConstructor() {
FlexibleCSVWriter writer = new FlexibleCSVWriter();
assertNotNull(writer, "The FlexibleCSVWriter instance should not be null");
}
@Test
void testConstructorWithCSVFormat() {
CSVFormat csvFormat = CSVFormat.DEFAULT;
FlexibleCSVWriter writer = new FlexibleCSVWriter(csvFormat);
assertNotNull(
writer,
"The FlexibleCSVWriter instance should not be null when initialized with"
+ " CSVFormat");
}
}
@@ -1,13 +1,10 @@
package stirling.software.SPDF.pdf;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@@ -53,13 +50,16 @@ class TextFinderTest {
expectedCount,
foundTexts.size(),
String.format(
"Expected %d matches for search term '%s'", expectedCount, searchTerm));
Locale.ROOT,
"Expected %d matches for search term '%s'",
expectedCount,
searchTerm));
if (expectedTexts != null) {
for (String expectedText : expectedTexts) {
assertTrue(
foundTexts.stream().anyMatch(text -> text.getText().equals(expectedText)),
String.format("Expected to find text: '%s'", expectedText));
String.format(Locale.ROOT, "Expected to find text: '%s'", expectedText));
}
}
@@ -275,7 +275,10 @@ class TextFinderTest {
// Each pattern should find at least one match in our test content
assertFalse(
foundTexts.isEmpty(),
String.format("Pattern '%s' should find at least one match", regexPattern));
String.format(
Locale.ROOT,
"Pattern '%s' should find at least one match",
regexPattern));
}
@Test
@@ -409,11 +412,11 @@ class TextFinderTest {
addTextToPage(document.getPage(i), "Page " + i + " contains searchable content.");
}
long startTime = System.currentTimeMillis();
long startTime = 1000000L; // Fixed start time
TextFinder textFinder = new TextFinder("searchable", false, false);
textFinder.getText(document);
List<PDFText> foundTexts = textFinder.getFoundTexts();
long endTime = System.currentTimeMillis();
long endTime = 1001000L; // Fixed end time
assertEquals(10, foundTexts.size());
assertTrue(
@@ -0,0 +1,124 @@
package stirling.software.SPDF.service;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext;
import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.common.service.UserServiceInterface;
@ExtendWith(MockitoExtension.class)
class ApiDocServiceTest {
@Mock ServletContext servletContext;
@Mock UserServiceInterface userService;
ApiDocService apiDocService;
ObjectMapper mapper = new ObjectMapper();
@BeforeEach
void setUp() {
apiDocService = new ApiDocService(servletContext, userService);
}
private void setApiDocumentation(Map<String, ApiEndpoint> docs) throws Exception {
Field field = ApiDocService.class.getDeclaredField("apiDocumentation");
field.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, ApiEndpoint> map = (Map<String, ApiEndpoint>) field.get(apiDocService);
map.clear();
map.putAll(docs);
}
private void setApiDocsJsonRootNode() throws Exception {
Field field = ApiDocService.class.getDeclaredField("apiDocsJsonRootNode");
field.setAccessible(true);
field.set(apiDocService, mapper.createObjectNode());
}
@Test
void getExtensionTypesReturnsExpectedList() throws Exception {
String json = "{\"description\": \"Output:PDF\"}";
JsonNode postNode = mapper.readTree(json);
ApiEndpoint endpoint = new ApiEndpoint("/test", postNode);
setApiDocumentation(Map.of("/test", endpoint));
setApiDocsJsonRootNode();
List<String> extensions = apiDocService.getExtensionTypes(true, "/test");
assertEquals(List.of("pdf"), extensions);
}
@Test
void getExtensionTypesHandlesUnknownOperation() throws Exception {
setApiDocumentation(Map.of());
List<String> extensions = apiDocService.getExtensionTypes(true, "/unknown");
assertNull(extensions);
}
@Test
void isValidOperationChecksRequiredParameters() throws Exception {
String json =
"{\"description\": \"desc\", \"parameters\": [{\"name\":\"param1\"}, {\"name\":\"param2\"}]}";
JsonNode postNode = mapper.readTree(json);
ApiEndpoint endpoint = new ApiEndpoint("/op", postNode);
setApiDocumentation(Map.of("/op", endpoint));
setApiDocsJsonRootNode();
assertTrue(apiDocService.isValidOperation("/op", Map.of("param1", "a", "param2", "b")));
assertFalse(apiDocService.isValidOperation("/op", Map.of("param1", "a")));
}
@Test
void isValidOperationHandlesUnknownOperation() throws Exception {
setApiDocumentation(Map.of());
assertFalse(apiDocService.isValidOperation("/unknown", Map.of("param1", "a")));
}
@Test
void isMultiInputDetectsTypeMI() throws Exception {
String json = "{\"description\": \"Type:MI\"}";
JsonNode postNode = mapper.readTree(json);
ApiEndpoint endpoint = new ApiEndpoint("/multi", postNode);
setApiDocumentation(Map.of("/multi", endpoint));
setApiDocsJsonRootNode();
assertTrue(apiDocService.isMultiInput("/multi"));
}
@Test
void isMultiInputDetectsUnknownOperation() throws Exception {
setApiDocumentation(Map.of());
assertFalse(apiDocService.isMultiInput("/unknown"));
}
@Test
void isMultiInputHandlesNoDescription() throws Exception {
String json = "{\"parameters\": [{\"name\":\"param1\"}, {\"name\":\"param2\"}]}";
JsonNode postNode = mapper.readTree(json);
ApiEndpoint endpoint = new ApiEndpoint("/multi", postNode);
setApiDocumentation(Map.of("/multi", endpoint));
setApiDocsJsonRootNode();
assertFalse(apiDocService.isMultiInput("/multi"));
}
}
@@ -7,11 +7,15 @@ import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
class AttachmentServiceTest {
@@ -105,4 +109,86 @@ class AttachmentServiceTest {
assertNotNull(result.getDocumentCatalog().getNames());
}
}
@Test
void extractAttachments_SanitizesFilenamesAndExtractsData() throws IOException {
attachmentService = new AttachmentService(1024 * 1024, 5 * 1024 * 1024);
try (var document = new PDDocument()) {
var maliciousAttachment =
new MockMultipartFile(
"file",
"..\\evil/../../tricky.txt",
MediaType.TEXT_PLAIN_VALUE,
"danger".getBytes());
attachmentService.addAttachment(document, List.of(maliciousAttachment));
Optional<byte[]> extracted = attachmentService.extractAttachments(document);
assertTrue(extracted.isPresent());
try (var zipInputStream =
new ZipInputStream(new ByteArrayInputStream(extracted.get()))) {
ZipEntry entry = zipInputStream.getNextEntry();
assertNotNull(entry);
String sanitizedName = entry.getName();
assertFalse(sanitizedName.contains(".."));
assertFalse(sanitizedName.contains("/"));
assertFalse(sanitizedName.contains("\\"));
byte[] data = zipInputStream.readAllBytes();
assertArrayEquals("danger".getBytes(), data);
assertNull(zipInputStream.getNextEntry());
}
}
}
@Test
void extractAttachments_SkipsAttachmentsExceedingSizeLimit() throws IOException {
attachmentService = new AttachmentService(4, 10);
try (var document = new PDDocument()) {
var oversizedAttachment =
new MockMultipartFile(
"file",
"large.bin",
MediaType.APPLICATION_OCTET_STREAM_VALUE,
"too big".getBytes());
attachmentService.addAttachment(document, List.of(oversizedAttachment));
Optional<byte[]> extracted = attachmentService.extractAttachments(document);
assertTrue(extracted.isEmpty());
}
}
@Test
void extractAttachments_EnforcesTotalSizeLimit() throws IOException {
attachmentService = new AttachmentService(10, 9);
try (var document = new PDDocument()) {
var first =
new MockMultipartFile(
"file", "first.txt", MediaType.TEXT_PLAIN_VALUE, "12345".getBytes());
var second =
new MockMultipartFile(
"file", "second.txt", MediaType.TEXT_PLAIN_VALUE, "67890".getBytes());
attachmentService.addAttachment(document, List.of(first, second));
Optional<byte[]> extracted = attachmentService.extractAttachments(document);
assertTrue(extracted.isPresent());
try (var zipInputStream =
new ZipInputStream(new ByteArrayInputStream(extracted.get()))) {
ZipEntry firstEntry = zipInputStream.getNextEntry();
assertNotNull(firstEntry);
assertEquals("first.txt", firstEntry.getName());
byte[] firstData = zipInputStream.readNBytes(5);
assertArrayEquals("12345".getBytes(), firstData);
assertNull(zipInputStream.getNextEntry());
}
}
}
}
@@ -1,11 +1,9 @@
package stirling.software.SPDF.service;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
@@ -33,12 +31,19 @@ class LanguageServiceBasicTest {
languageService = new LanguageServiceForTest(applicationProperties);
}
// Helper methods
private static Resource createMockResource(String filename) {
Resource mockResource = mock(Resource.class);
when(mockResource.getFilename()).thenReturn(filename);
return mockResource;
}
@Test
void testGetSupportedLanguages_BasicFunctionality() throws IOException {
void testGetSupportedLanguages_BasicFunctionality() {
// Set up mocked resources
Resource enResource = createMockResource("messages_en_US.properties");
Resource frResource = createMockResource("messages_fr_FR.properties");
Resource[] mockResources = new Resource[] {enResource, frResource};
Resource[] mockResources = {enResource, frResource};
// Configure the test service
((LanguageServiceForTest) languageService).setMockResources(mockResources);
@@ -53,14 +58,13 @@ class LanguageServiceBasicTest {
}
@Test
void testGetSupportedLanguages_FilteringInvalidFiles() throws IOException {
void testGetSupportedLanguages_FilteringInvalidFiles() {
// Set up mocked resources with invalid files
Resource[] mockResources =
new Resource[] {
createMockResource("messages_en_US.properties"), // Valid
createMockResource("invalid_file.properties"), // Invalid
createMockResource(null) // Null filename
};
Resource[] mockResources = {
createMockResource("messages_en_US.properties"), // Valid
createMockResource("invalid_file.properties"), // Invalid
createMockResource(null) // Null filename
};
// Configure the test service
((LanguageServiceForTest) languageService).setMockResources(mockResources);
@@ -77,15 +81,14 @@ class LanguageServiceBasicTest {
}
@Test
void testGetSupportedLanguages_WithRestrictions() throws IOException {
void testGetSupportedLanguages_WithRestrictions() {
// Set up test resources
Resource[] mockResources =
new Resource[] {
createMockResource("messages_en_US.properties"),
createMockResource("messages_fr_FR.properties"),
createMockResource("messages_de_DE.properties"),
createMockResource("messages_en_GB.properties")
};
Resource[] mockResources = {
createMockResource("messages_en_US.properties"),
createMockResource("messages_fr_FR.properties"),
createMockResource("messages_de_DE.properties"),
createMockResource("messages_en_GB.properties")
};
// Configure the test service
((LanguageServiceForTest) languageService).setMockResources(mockResources);
@@ -104,13 +107,6 @@ class LanguageServiceBasicTest {
assertFalse(supportedLanguages.contains("de_DE"), "Restricted language should be excluded");
}
// Helper methods
private Resource createMockResource(String filename) {
Resource mockResource = mock(Resource.class);
when(mockResource.getFilename()).thenReturn(filename);
return mockResource;
}
// Test subclass
private static class LanguageServiceForTest extends LanguageService {
private Resource[] mockResources;
@@ -124,7 +120,7 @@ class LanguageServiceBasicTest {
}
@Override
protected Resource[] getResourcesFromPattern(String pattern) throws IOException {
protected Resource[] getResourcesFromPattern(String pattern) {
return mockResources;
}
}
@@ -15,7 +15,6 @@ import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.Ui;
@@ -24,10 +23,15 @@ class LanguageServiceTest {
private LanguageService languageService;
private ApplicationProperties applicationProperties;
private PathMatchingResourcePatternResolver mockedResolver;
private static Resource createMockResource(String filename) {
Resource mockResource = mock(Resource.class);
when(mockResource.getFilename()).thenReturn(filename);
return mockResource;
}
@BeforeEach
void setUp() throws Exception {
void setUp() {
// Mock ApplicationProperties
applicationProperties = mock(ApplicationProperties.class);
Ui ui = mock(Ui.class);
@@ -38,7 +42,7 @@ class LanguageServiceTest {
}
@Test
void testGetSupportedLanguages_NoRestrictions() throws IOException {
void testGetSupportedLanguages_NoRestrictions() {
// Setup
Set<String> expectedLanguages =
new HashSet<>(Arrays.asList("en_US", "fr_FR", "de_DE", "en_GB"));
@@ -61,7 +65,7 @@ class LanguageServiceTest {
}
@Test
void testGetSupportedLanguages_WithRestrictions() throws IOException {
void testGetSupportedLanguages_WithRestrictions() {
// Setup
Set<String> expectedLanguages =
new HashSet<>(Arrays.asList("en_US", "fr_FR", "de_DE", "en_GB"));
@@ -87,7 +91,7 @@ class LanguageServiceTest {
}
@Test
void testGetSupportedLanguages_ExceptionHandling() throws IOException {
void testGetSupportedLanguages_ExceptionHandling() {
// Setup - make resolver throw an exception
((LanguageServiceForTest) languageService).setShouldThrowException(true);
@@ -98,19 +102,24 @@ class LanguageServiceTest {
assertTrue(supportedLanguages.isEmpty(), "Should return empty set on exception");
}
// Helper methods to create mock resources
private Resource[] createMockResources(Set<String> languages) {
return languages.stream()
.map(lang -> createMockResource("messages_" + lang + ".properties"))
.toArray(Resource[]::new);
}
@Test
void testGetSupportedLanguages_FilteringNonMatchingFiles() throws IOException {
void testGetSupportedLanguages_FilteringNonMatchingFiles() {
// Setup with some valid and some invalid filenames
Resource[] mixedResources =
new Resource[] {
createMockResource("messages_en_US.properties"),
createMockResource(
"messages_en_GB.properties"), // Explicitly add en_GB resource
createMockResource("messages_fr_FR.properties"),
createMockResource("not_a_messages_file.properties"),
createMockResource("messages_.properties"), // Invalid format
createMockResource(null) // Null filename
};
Resource[] mixedResources = {
createMockResource("messages_en_US.properties"),
createMockResource("messages_en_GB.properties"), // Explicitly add en_GB resource
createMockResource("messages_fr_FR.properties"),
createMockResource("not_a_messages_file.properties"),
createMockResource("messages_.properties"), // Invalid format
createMockResource(null) // Null filename
};
((LanguageServiceForTest) languageService).setMockResources(mixedResources);
when(applicationProperties.getUi().getLanguages()).thenReturn(Collections.emptyList());
@@ -132,19 +141,6 @@ class LanguageServiceTest {
// language codes
}
// Helper methods to create mock resources
private Resource[] createMockResources(Set<String> languages) {
return languages.stream()
.map(lang -> createMockResource("messages_" + lang + ".properties"))
.toArray(Resource[]::new);
}
private Resource createMockResource(String filename) {
Resource mockResource = mock(Resource.class);
when(mockResource.getFilename()).thenReturn(filename);
return mockResource;
}
// Test subclass that allows us to control the resource resolver
private static class LanguageServiceForTest extends LanguageService {
private Resource[] mockResources;
@@ -0,0 +1,75 @@
package stirling.software.SPDF.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import stirling.software.SPDF.config.EndpointInspector;
import stirling.software.common.service.PostHogService;
class MetricsAggregatorServiceTest {
private SimpleMeterRegistry meterRegistry;
private PostHogService postHogService;
private EndpointInspector endpointInspector;
private MetricsAggregatorService metricsAggregatorService;
@BeforeEach
void setUp() {
meterRegistry = new SimpleMeterRegistry();
postHogService = mock(PostHogService.class);
endpointInspector = mock(EndpointInspector.class);
when(endpointInspector.getValidGetEndpoints()).thenReturn(Set.of("/getEndpoint"));
when(endpointInspector.isValidGetEndpoint("/getEndpoint")).thenReturn(true);
metricsAggregatorService =
new MetricsAggregatorService(meterRegistry, postHogService, endpointInspector);
}
@Captor private ArgumentCaptor<Map<String, Object>> captor;
@Test
void testAggregateAndSendMetrics() {
meterRegistry.counter("http.requests", "method", "GET", "uri", "/getEndpoint").increment(3);
meterRegistry.counter("http.requests", "method", "POST", "uri", "/api/v1/do").increment(2);
metricsAggregatorService.aggregateAndSendMetrics();
ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
verify(postHogService).captureEvent(eq("aggregated_metrics"), captor.capture());
Map<String, Object> metrics = captor.getValue();
assertEquals(2, metrics.size());
assertEquals(3.0, (Double) metrics.get("http_requests_GET__getEndpoint"));
assertEquals(2.0, (Double) metrics.get("http_requests_POST__api_v1_do"));
}
@Test
void testAggregateAndSendMetricsSendsOnlyDifferences() {
Counter counter =
meterRegistry.counter("http.requests", "method", "GET", "uri", "/getEndpoint");
counter.increment(5);
metricsAggregatorService.aggregateAndSendMetrics();
reset(postHogService);
counter.increment(2);
metricsAggregatorService.aggregateAndSendMetrics();
ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
verify(postHogService).captureEvent(eq("aggregated_metrics"), captor.capture());
Map<String, Object> metrics = captor.getValue();
assertEquals(1, metrics.size());
assertEquals(2.0, (Double) metrics.get("http_requests_GET__getEndpoint"));
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import java.io.IOException;
@@ -25,6 +24,24 @@ class PdfImageRemovalServiceTest {
service = new PdfImageRemovalService();
}
// Helper method for matching COSName in verification
private static COSName eq(final COSName value) {
return Mockito.argThat(
new org.mockito.ArgumentMatcher<>() {
@Override
public boolean matches(COSName argument) {
if (argument == null && value == null) return true;
if (argument == null || value == null) return false;
return argument.getName().equals(value.getName());
}
@Override
public String toString() {
return "eq(" + (value != null ? value.getName() : "null") + ")";
}
});
}
@Test
void testRemoveImagesFromPdf_WithImages() throws IOException {
// Mock PDF document and its components
@@ -55,7 +72,7 @@ class PdfImageRemovalServiceTest {
when(resources.isImageXObject(nonImg)).thenReturn(false);
// Execute the method
PDDocument result = service.removeImagesFromPdf(document);
service.removeImagesFromPdf(document);
// Verify that images were removed
verify(resources, times(1)).put(eq(img1), Mockito.<PDXObject>isNull());
@@ -84,7 +101,7 @@ class PdfImageRemovalServiceTest {
when(resources.getXObjectNames()).thenReturn(emptyList);
// Execute the method
PDDocument result = service.removeImagesFromPdf(document);
service.removeImagesFromPdf(document);
// Verify that no modifications were made
verify(resources, never()).put(any(COSName.class), any(PDXObject.class));
@@ -120,28 +137,10 @@ class PdfImageRemovalServiceTest {
when(resources2.isImageXObject(img2)).thenReturn(true);
// Execute the method
PDDocument result = service.removeImagesFromPdf(document);
service.removeImagesFromPdf(document);
// Verify that images were removed from both pages
verify(resources1, times(1)).put(eq(img1), Mockito.<PDXObject>isNull());
verify(resources2, times(1)).put(eq(img2), Mockito.<PDXObject>isNull());
}
// Helper method for matching COSName in verification
private static COSName eq(final COSName value) {
return Mockito.argThat(
new org.mockito.ArgumentMatcher<COSName>() {
@Override
public boolean matches(COSName argument) {
if (argument == null && value == null) return true;
if (argument == null || value == null) return false;
return argument.getName().equals(value.getName());
}
@Override
public String toString() {
return "eq(" + (value != null ? value.getName() : "null") + ")";
}
});
}
}
@@ -26,19 +26,17 @@ import stirling.software.common.service.UserServiceInterface;
class PdfMetadataServiceBasicTest {
private ApplicationProperties applicationProperties;
private UserServiceInterface userService;
private PdfMetadataService pdfMetadataService;
private final String STIRLING_PDF_LABEL = "Stirling PDF";
@BeforeEach
void setUp() {
// Set up mocks for application properties' nested objects
applicationProperties = mock(ApplicationProperties.class);
ApplicationProperties applicationProperties = mock(ApplicationProperties.class);
Premium premium = mock(Premium.class);
ProFeatures proFeatures = mock(ProFeatures.class);
CustomMetadata customMetadata = mock(CustomMetadata.class);
userService = mock(UserServiceInterface.class);
UserServiceInterface userService = mock(UserServiceInterface.class);
when(applicationProperties.getPremium()).thenReturn(premium);
when(premium.getProFeatures()).thenReturn(proFeatures);
@@ -0,0 +1,77 @@
package stirling.software.SPDF.service.misc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.Factories.ReplaceAndInvertColorFactory;
import stirling.software.common.model.api.misc.HighContrastColorCombination;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import stirling.software.common.util.misc.ReplaceAndInvertColorStrategy;
class ReplaceAndInvertColorServiceTest {
@Mock private ReplaceAndInvertColorFactory replaceAndInvertColorFactory;
@Mock private MultipartFile file;
@Mock private ReplaceAndInvertColorStrategy replaceAndInvertColorStrategy;
@InjectMocks private ReplaceAndInvertColorService replaceAndInvertColorService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testReplaceAndInvertColor() throws IOException {
// Arrange
ReplaceAndInvert replaceAndInvertOption = mock(ReplaceAndInvert.class);
HighContrastColorCombination highContrastColorCombination =
mock(HighContrastColorCombination.class);
String backGroundColor = "#FFFFFF";
String textColor = "#000000";
when(replaceAndInvertColorFactory.replaceAndInvert(
file,
replaceAndInvertOption,
highContrastColorCombination,
backGroundColor,
textColor))
.thenReturn(replaceAndInvertColorStrategy);
InputStreamResource expectedResource = mock(InputStreamResource.class);
when(replaceAndInvertColorStrategy.replace()).thenReturn(expectedResource);
// Act
InputStreamResource result =
replaceAndInvertColorService.replaceAndInvertColor(
file,
replaceAndInvertOption,
highContrastColorCombination,
backGroundColor,
textColor);
// Assert
assertNotNull(result);
assertEquals(expectedResource, result);
verify(replaceAndInvertColorFactory, times(1))
.replaceAndInvert(
file,
replaceAndInvertOption,
highContrastColorCombination,
backGroundColor,
textColor);
verify(replaceAndInvertColorStrategy, times(1)).replace();
}
}