mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
V1 merge (#5193)
# 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:
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
@@ -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();
|
||||
}
|
||||
}
|
||||
+93
@@ -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);
|
||||
}
|
||||
}
|
||||
+59
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-22
@@ -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());
|
||||
|
||||
|
||||
+2
-6
@@ -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);
|
||||
}
|
||||
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -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(
|
||||
|
||||
+266
@@ -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) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+328
@@ -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) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+571
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
-32
@@ -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 A–Z, a–z, 0–9, Unterstrich und Punkt erlaubt
|
||||
// Only A–Z, a–z, 0–9, 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -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);
|
||||
|
||||
+146
@@ -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
@@ -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;
|
||||
|
||||
+13
-9
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+843
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+333
-336
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+231
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-28
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+109
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-28
@@ -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;
|
||||
|
||||
+75
@@ -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"));
|
||||
}
|
||||
}
|
||||
+21
-22
@@ -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") + ")";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -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);
|
||||
|
||||
+77
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user