New common module (#3573)

# Description of Changes

Introduced a new `common` module for shared libs and commonly used
classes. See the screenshot below for the file structure and classes
that have been moved.

---
<img width="452" alt="Screenshot 2025-05-22 at 11 46 56"
src="https://github.com/user-attachments/assets/c9badabc-48f9-4079-b83e-7cfde0fb840f"
/>
<img width="470" alt="Screenshot 2025-05-22 at 11 47 30"
src="https://github.com/user-attachments/assets/e8315b09-2e78-4c50-b9de-4dd9b9b0ecb1"
/>

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] 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/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

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

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Dario Ghunney Ware
2025-05-27 13:01:52 +01:00
committed by GitHub
parent be1a9cc8da
commit bedc3d02d7
248 changed files with 1429 additions and 1011 deletions

View File

@@ -0,0 +1,223 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
import java.io.*;
import java.nio.file.*;
import java.nio.file.Files;
import java.util.Arrays;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.common.model.api.PDFFile;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Execution(value = ExecutionMode.SAME_THREAD)
class CustomPDFDocumentFactoryTest {
private SpyPDFDocumentFactory factory;
private byte[] basePdfBytes;
@BeforeEach
void setup() throws IOException {
PdfMetadataService mockService = mock(PdfMetadataService.class);
factory = new SpyPDFDocumentFactory(mockService);
try (InputStream is = getClass().getResourceAsStream("/example.pdf")) {
assertNotNull(is, "example.pdf must be present in src/test/resources");
basePdfBytes = is.readAllBytes();
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
try (PDDocument doc = factory.load(file)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
try (PDDocument doc = factory.load(inflated)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
try (PDDocument doc = factory.load(multipart)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
@ParameterizedTest
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
PDFFile pdfFile = new PDFFile();
pdfFile.setFileInput(multipart);
try (PDDocument doc = factory.load(pdfFile)) {
Assertions.assertEquals(expected, factory.lastStrategyUsed);
}
}
private byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
try (PDDocument doc = Loader.loadPDF(input)) {
byte[] largeData = new byte[sizeInMB * 1024 * 1024];
Arrays.fill(largeData, (byte) 'A');
PDStream stream = new PDStream(doc, new ByteArrayInputStream(largeData));
stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT);
stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE);
doc.getDocumentCatalog()
.getCOSObject()
.setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject());
ByteArrayOutputStream out = new ByteArrayOutputStream();
doc.save(out);
return out.toByteArray();
}
}
@Test
void testLoadFromPath() throws IOException {
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
Path path = file.toPath();
try (PDDocument doc = factory.load(path)) {
assertNotNull(doc);
}
}
@Test
void testLoadFromStringPath() throws IOException {
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
try (PDDocument doc = factory.load(file.getAbsolutePath())) {
assertNotNull(doc);
}
}
// neeed to add password pdf
// @Test
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
// try (PDDocument doc = factory.load(is, "test123")) {
// assertNotNull(doc);
// }
// }
// }
//
// @Test
// void testLoadPasswordProtectedPdfFromMultipart() throws IOException {
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
// byte[] bytes = is.readAllBytes();
// MockMultipartFile file = new MockMultipartFile("file", "protected.pdf",
// "application/pdf", bytes);
// try (PDDocument doc = factory.load(file, "test123")) {
// assertNotNull(doc);
// }
// }
// }
@Test
void testLoadReadOnlySkipsPostProcessing() throws IOException {
PdfMetadataService mockService = mock(PdfMetadataService.class);
CustomPDFDocumentFactory readOnlyFactory = new CustomPDFDocumentFactory(mockService);
byte[] bytes = inflatePdf(basePdfBytes, 5);
try (PDDocument doc = readOnlyFactory.load(bytes, true)) {
assertNotNull(doc);
verify(mockService, never()).setDefaultMetadata(any());
}
}
@Test
void testCreateNewDocument() throws IOException {
try (PDDocument doc = factory.createNewDocument()) {
assertNotNull(doc);
}
}
@Test
void testCreateNewDocumentBasedOnOldDocument() throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, 5);
try (PDDocument oldDoc = Loader.loadPDF(inflated);
PDDocument newDoc = factory.createNewDocumentBasedOnOldDocument(oldDoc)) {
assertNotNull(newDoc);
}
}
@Test
void testLoadToBytesRoundTrip() throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, 5);
File file = writeTempFile(inflated);
byte[] resultBytes = factory.loadToBytes(file);
try (PDDocument doc = Loader.loadPDF(resultBytes)) {
assertNotNull(doc);
assertTrue(doc.getNumberOfPages() > 0);
}
}
@Test
void testSaveToBytesAndReload() throws IOException {
try (PDDocument doc = Loader.loadPDF(basePdfBytes)) {
byte[] saved = factory.saveToBytes(doc);
try (PDDocument reloaded = Loader.loadPDF(saved)) {
assertNotNull(reloaded);
assertEquals(doc.getNumberOfPages(), reloaded.getNumberOfPages());
}
}
}
@Test
void testCreateNewBytesBasedOnOldDocument() throws IOException {
byte[] newBytes = factory.createNewBytesBasedOnOldDocument(basePdfBytes);
assertNotNull(newBytes);
assertTrue(newBytes.length > 0);
}
private File writeTempFile(byte[] content) throws IOException {
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
Files.write(file.toPath(), content);
return file;
}
@BeforeEach
void cleanup() {
System.gc();
}
}

View File

@@ -0,0 +1,31 @@
package stirling.software.common.service;
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
enum StrategyType {
MEMORY_ONLY,
MIXED,
TEMP_FILE
}
public StrategyType lastStrategyUsed;
public SpyPDFDocumentFactory(PdfMetadataService service) {
super(service);
}
@Override
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
StrategyType type;
if (contentSize < 10 * 1024 * 1024) {
type = StrategyType.MEMORY_ONLY;
} else if (contentSize < 50 * 1024 * 1024) {
type = StrategyType.MIXED;
} else {
type = StrategyType.TEMP_FILE;
}
this.lastStrategyUsed = type;
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
}
}

View File

@@ -0,0 +1,206 @@
package stirling.software.common.util;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class CheckProgramInstallTest {
private MockedStatic<ProcessExecutor> mockProcessExecutor;
private ProcessExecutor mockExecutor;
@BeforeEach
void setUp() throws Exception {
// Reset static variables before each test
resetStaticFields();
// Set up mock for ProcessExecutor
mockExecutor = Mockito.mock(ProcessExecutor.class);
mockProcessExecutor = mockStatic(ProcessExecutor.class);
mockProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV))
.thenReturn(mockExecutor);
}
@AfterEach
void tearDown() {
// Close the static mock to prevent memory leaks
if (mockProcessExecutor != null) {
mockProcessExecutor.close();
}
}
/** Reset static fields in the CheckProgramInstall class using reflection */
private void resetStaticFields() throws Exception {
Field pythonAvailableCheckedField =
CheckProgramInstall.class.getDeclaredField("pythonAvailableChecked");
pythonAvailableCheckedField.setAccessible(true);
pythonAvailableCheckedField.set(null, false);
Field availablePythonCommandField =
CheckProgramInstall.class.getDeclaredField("availablePythonCommand");
availablePythonCommandField.setAccessible(true);
availablePythonCommandField.set(null, null);
}
@Test
void testGetAvailablePythonCommand_WhenPython3IsAvailable()
throws IOException, InterruptedException {
// Arrange
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
when(result.getRc()).thenReturn(0);
when(result.getMessages()).thenReturn("Python 3.9.0");
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
.thenReturn(result);
// Act
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
// Assert
assertEquals("python3", pythonCommand);
assertTrue(CheckProgramInstall.isPythonAvailable());
// Verify that the command was executed
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
}
@Test
void testGetAvailablePythonCommand_WhenPython3IsNotAvailableButPythonIs()
throws IOException, InterruptedException {
// Arrange
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
.thenThrow(new IOException("Command not found"));
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
when(result.getRc()).thenReturn(0);
when(result.getMessages()).thenReturn("Python 2.7.0");
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python", "--version")))
.thenReturn(result);
// Act
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
// Assert
assertEquals("python", pythonCommand);
assertTrue(CheckProgramInstall.isPythonAvailable());
// Verify that both commands were attempted
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python", "--version"));
}
@Test
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode()
throws IOException, InterruptedException, Exception {
// Arrange
// Reset the static fields again to ensure clean state
resetStaticFields();
// Since we want to test the scenario where Python returns a non-zero exit code
// We need to make sure both python3 and python commands are mocked to return failures
ProcessExecutorResult resultPython3 = Mockito.mock(ProcessExecutorResult.class);
when(resultPython3.getRc()).thenReturn(1); // Non-zero exit code
when(resultPython3.getMessages()).thenReturn("Error");
// Important: in the CheckProgramInstall implementation, only checks if
// command throws exception, it doesn't check the return code
// So we need to throw an exception instead
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
.thenThrow(new IOException("Command failed with non-zero exit code"));
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python", "--version")))
.thenThrow(new IOException("Command failed with non-zero exit code"));
// Act
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
// Assert - Both commands throw exceptions, so no python is available
assertNull(pythonCommand);
assertFalse(CheckProgramInstall.isPythonAvailable());
}
@Test
void testGetAvailablePythonCommand_WhenNoPythonIsAvailable()
throws IOException, InterruptedException {
// Arrange
when(mockExecutor.runCommandWithOutputHandling(any(List.class)))
.thenThrow(new IOException("Command not found"));
// Act
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
// Assert
assertNull(pythonCommand);
assertFalse(CheckProgramInstall.isPythonAvailable());
// Verify attempts to run both python3 and python
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python", "--version"));
}
@Test
void testGetAvailablePythonCommand_CachesResult() throws IOException, InterruptedException {
// Arrange
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
when(result.getRc()).thenReturn(0);
when(result.getMessages()).thenReturn("Python 3.9.0");
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
.thenReturn(result);
// Act
String firstCall = CheckProgramInstall.getAvailablePythonCommand();
// Change the mock to simulate a change in the environment
when(mockExecutor.runCommandWithOutputHandling(any(List.class)))
.thenThrow(new IOException("Command not found"));
String secondCall = CheckProgramInstall.getAvailablePythonCommand();
// Assert
assertEquals("python3", firstCall);
assertEquals("python3", secondCall); // Second call should return the cached result
// Verify python3 command was only executed once (caching worked)
verify(mockExecutor, times(1))
.runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
}
@Test
void testIsPythonAvailable_DirectCall() throws Exception {
// Arrange
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
when(result.getRc()).thenReturn(0);
when(result.getMessages()).thenReturn("Python 3.9.0");
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
.thenReturn(result);
// Reset again to ensure clean state
resetStaticFields();
// Act - Call isPythonAvailable() directly
boolean pythonAvailable = CheckProgramInstall.isPythonAvailable();
// Assert
assertTrue(pythonAvailable);
// Verify getAvailablePythonCommand was called internally
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
}
}

View File

@@ -0,0 +1,331 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class CustomHtmlSanitizerTest {
@ParameterizedTest
@MethodSource("provideHtmlTestCases")
void testSanitizeHtml(String inputHtml, String[] expectedContainedTags) {
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(inputHtml);
// Assert
for (String tag : expectedContainedTags) {
assertTrue(sanitizedHtml.contains(tag), tag + " should be preserved");
}
}
private static Stream<Arguments> provideHtmlTestCases() {
return Stream.of(
Arguments.of(
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
new String[] {"<p>", "<strong>", "<em>"}),
Arguments.of(
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
new String[] {
"<b>bold</b>",
"<i>italic</i>",
"<em>emphasis</em>",
"<strong>strong</strong>"
}),
Arguments.of(
"<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
+ "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
+ "<ol><li>Ordered item</li></ol>",
new String[] {
"<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"
}));
}
@Test
void testSanitizeAllowsStyles() {
// Arrange - Testing Sanitizers.STYLES
String htmlWithStyles =
"<p style=\"color: blue; font-size: 16px; margin-top: 10px;\">Styled text</p>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithStyles);
// Assert
// The OWASP HTML Sanitizer might filter some specific styles, so we only check that
// the sanitized HTML is not empty and contains a paragraph tag with style
assertTrue(sanitizedHtml.contains("<p"), "Paragraph tag should be preserved");
assertTrue(sanitizedHtml.contains("style="), "Style attribute should be preserved");
assertTrue(sanitizedHtml.contains("Styled text"), "Content should be preserved");
}
@Test
void testSanitizeAllowsLinks() {
// Arrange - Testing Sanitizers.LINKS
String htmlWithLink =
"<a href=\"https://example.com\" title=\"Example Site\">Example Link</a>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithLink);
// Assert
// The most important aspect is that the link content is preserved
assertTrue(sanitizedHtml.contains("Example Link"), "Link text should be preserved");
// Check that the href is present in some form
assertTrue(sanitizedHtml.contains("href="), "Link href attribute should be present");
// Check that the URL is present in some form
assertTrue(sanitizedHtml.contains("example.com"), "Link URL should be preserved");
// OWASP sanitizer may handle title attributes differently depending on version
// So we won't make strict assertions about the title attribute
}
@Test
void testSanitizeDisallowsJavaScriptLinks() {
// Arrange
String htmlWithJsLink = "<a href=\"javascript:alert('XSS')\">Malicious Link</a>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsLink);
// Assert
assertFalse(sanitizedHtml.contains("javascript:"), "JavaScript URLs should be removed");
// The link tag might still be there, but the href should be sanitized
assertTrue(sanitizedHtml.contains("Malicious Link"), "Link text should be preserved");
}
@Test
void testSanitizeAllowsTables() {
// Arrange - Testing Sanitizers.TABLES
String htmlWithTable =
"<table border=\"1\">"
+ "<thead><tr><th>Header 1</th><th>Header 2</th></tr></thead>"
+ "<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody>"
+ "<tfoot><tr><td colspan=\"2\">Footer</td></tr></tfoot>"
+ "</table>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithTable);
// Assert
assertTrue(sanitizedHtml.contains("<table"), "Table should be preserved");
assertTrue(sanitizedHtml.contains("<tr>"), "Table rows should be preserved");
assertTrue(sanitizedHtml.contains("<th>"), "Table headers should be preserved");
assertTrue(sanitizedHtml.contains("<td>"), "Table cells should be preserved");
// Note: border attribute might be removed as it's deprecated in HTML5
// Check for content values instead of exact tag formats because
// the sanitizer may normalize tags and attributes
assertTrue(sanitizedHtml.contains("Header 1"), "Table header content should be preserved");
assertTrue(sanitizedHtml.contains("Cell 1"), "Table cell content should be preserved");
assertTrue(sanitizedHtml.contains("Footer"), "Table footer content should be preserved");
// OWASP sanitizer may not preserve these structural elements or attributes in the same
// format
// So we check for the content rather than the exact structure
}
@Test
void testSanitizeAllowsImages() {
// Arrange - Testing Sanitizers.IMAGES
String htmlWithImage =
"<img src=\"image.jpg\" alt=\"An image\" width=\"100\" height=\"100\">";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithImage);
// Assert
assertTrue(sanitizedHtml.contains("<img"), "Image tag should be preserved");
assertTrue(sanitizedHtml.contains("src=\"image.jpg\""), "Image source should be preserved");
assertTrue(
sanitizedHtml.contains("alt=\"An image\""), "Image alt text should be preserved");
// Width and height might be preserved, but not guaranteed by all sanitizers
}
@Test
void testSanitizeDisallowsDataUrlImages() {
// Arrange
String htmlWithDataUrlImage =
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\" alt=\"SVG with XSS\">";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithDataUrlImage);
// Assert
assertFalse(
sanitizedHtml.contains("data:image/svg"),
"Data URLs with potentially malicious content should be removed");
}
@Test
void testSanitizeRemovesJavaScriptInAttributes() {
// Arrange
String htmlWithJsEvent =
"<a href=\"#\" onclick=\"alert('XSS')\" onmouseover=\"alert('XSS')\">Click me</a>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsEvent);
// Assert
assertFalse(
sanitizedHtml.contains("onclick"), "JavaScript event handlers should be removed");
assertFalse(
sanitizedHtml.contains("onmouseover"),
"JavaScript event handlers should be removed");
assertTrue(sanitizedHtml.contains("Click me"), "Link text should be preserved");
}
@Test
void testSanitizeRemovesScriptTags() {
// Arrange
String htmlWithScript = "<p>Safe content</p><script>alert('XSS');</script>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithScript);
// Assert
assertFalse(sanitizedHtml.contains("<script>"), "Script tags should be removed");
assertTrue(
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
}
@Test
void testSanitizeRemovesNoScriptTags() {
// Arrange - Testing the custom policy to disallow noscript
String htmlWithNoscript = "<p>Safe content</p><noscript>JavaScript is disabled</noscript>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithNoscript);
// Assert
assertFalse(sanitizedHtml.contains("<noscript>"), "Noscript tags should be removed");
assertTrue(
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
}
@Test
void testSanitizeRemovesIframes() {
// Arrange
String htmlWithIframe = "<p>Safe content</p><iframe src=\"https://example.com\"></iframe>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithIframe);
// Assert
assertFalse(sanitizedHtml.contains("<iframe"), "Iframe tags should be removed");
assertTrue(
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
}
@Test
void testSanitizeRemovesObjectAndEmbed() {
// Arrange
String htmlWithObjects =
"<p>Safe content</p>"
+ "<object data=\"data.swf\" type=\"application/x-shockwave-flash\"></object>"
+ "<embed src=\"embed.swf\" type=\"application/x-shockwave-flash\">";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithObjects);
// Assert
assertFalse(sanitizedHtml.contains("<object"), "Object tags should be removed");
assertFalse(sanitizedHtml.contains("<embed"), "Embed tags should be removed");
assertTrue(
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
}
@Test
void testSanitizeRemovesMetaAndBaseAndLink() {
// Arrange
String htmlWithMetaTags =
"<p>Safe content</p>"
+ "<meta http-equiv=\"refresh\" content=\"0; url=http://evil.com\">"
+ "<base href=\"http://evil.com/\">"
+ "<link rel=\"stylesheet\" href=\"evil.css\">";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithMetaTags);
// Assert
assertFalse(sanitizedHtml.contains("<meta"), "Meta tags should be removed");
assertFalse(sanitizedHtml.contains("<base"), "Base tags should be removed");
assertFalse(sanitizedHtml.contains("<link"), "Link tags should be removed");
assertTrue(
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
}
@Test
void testSanitizeHandlesComplexHtml() {
// Arrange
String complexHtml =
"<div class=\"container\">"
+ " <h1 style=\"color: blue;\">Welcome</h1>"
+ " <p>This is a <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p>"
+ " <table>"
+ " <tr><th>Name</th><th>Value</th></tr>"
+ " <tr><td>Item 1</td><td>100</td></tr>"
+ " </table>"
+ " <img src=\"image.jpg\" alt=\"Test image\">"
+ " <script>alert('XSS');</script>"
+ " <iframe src=\"https://evil.com\"></iframe>"
+ "</div>";
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(complexHtml);
// Assert
assertTrue(sanitizedHtml.contains("<div"), "Div should be preserved");
assertTrue(sanitizedHtml.contains("<h1"), "H1 should be preserved");
assertTrue(
sanitizedHtml.contains("<strong>") && sanitizedHtml.contains("test"),
"Strong tag should be preserved");
// Check for content rather than exact formatting
assertTrue(
sanitizedHtml.contains("<a")
&& sanitizedHtml.contains("href=")
&& sanitizedHtml.contains("example.com")
&& sanitizedHtml.contains("link"),
"Link should be preserved");
assertTrue(sanitizedHtml.contains("<table"), "Table should be preserved");
assertTrue(sanitizedHtml.contains("<img"), "Image should be preserved");
assertFalse(sanitizedHtml.contains("<script>"), "Script tag should be removed");
assertFalse(sanitizedHtml.contains("<iframe"), "Iframe tag should be removed");
// Content checks
assertTrue(sanitizedHtml.contains("Welcome"), "Heading content should be preserved");
assertTrue(sanitizedHtml.contains("Name"), "Table header content should be preserved");
assertTrue(sanitizedHtml.contains("Item 1"), "Table data content should be preserved");
}
@Test
void testSanitizeHandlesEmpty() {
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize("");
// Assert
assertEquals("", sanitizedHtml, "Empty input should result in empty string");
}
@Test
void testSanitizeHandlesNull() {
// Act
String sanitizedHtml = CustomHtmlSanitizer.sanitize(null);
// Assert
assertEquals("", sanitizedHtml, "Null input should result in empty string");
}
}

View File

@@ -0,0 +1,45 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
public class ErrorUtilsTest {
@Test
public void testExceptionToModel() {
// Create a mock Model
Model model = new org.springframework.ui.ExtendedModelMap();
// Create a test exception
Exception ex = new Exception("Test Exception");
// Call the method under test
Model resultModel = ErrorUtils.exceptionToModel(model, ex);
// Verify the result
assertNotNull(resultModel);
assertEquals("Test Exception", resultModel.getAttribute("errorMessage"));
assertNotNull(resultModel.getAttribute("stackTrace"));
}
@Test
public void testExceptionToModelView() {
// Create a mock Model
Model model = new org.springframework.ui.ExtendedModelMap();
// Create a test exception
Exception ex = new Exception("Test Exception");
// Call the method under test
ModelAndView modelAndView = ErrorUtils.exceptionToModelView(model, ex);
// Verify the result
assertNotNull(modelAndView);
assertEquals("Test Exception", modelAndView.getModel().get("errorMessage"));
assertNotNull(modelAndView.getModel().get("stackTrace"));
}
}

View File

@@ -0,0 +1,35 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDateTime;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import stirling.software.common.model.FileInfo;
public class FileInfoTest {
@ParameterizedTest(name = "{index}: fileSize={0}")
@CsvSource({
"0, '0 Bytes'",
"1023, '1023 Bytes'",
"1024, '1.00 KB'",
"1048575, '1024.00 KB'", // Do we really want this as result?
"1048576, '1.00 MB'",
"1073741823, '1024.00 MB'", // Do we really want this as result?
"1073741824, '1.00 GB'"
})
void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) {
FileInfo fileInfo =
new FileInfo(
"example.txt",
"/path/to/example.txt",
LocalDateTime.now(),
fileSize,
LocalDateTime.now().minusDays(1));
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
}
}

View File

@@ -0,0 +1,176 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.function.Predicate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.common.configuration.RuntimePathConfig;
@ExtendWith(MockitoExtension.class)
class FileMonitorTest {
@TempDir Path tempDir;
@Mock private RuntimePathConfig runtimePathConfig;
@Mock private Predicate<Path> pathFilter;
private FileMonitor fileMonitor;
@BeforeEach
void setUp() throws IOException {
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
// This mock is used in all tests except testPathFilter
// We use lenient to avoid UnnecessaryStubbingException in that test
Mockito.lenient().when(pathFilter.test(any())).thenReturn(true);
fileMonitor = new FileMonitor(pathFilter, runtimePathConfig);
}
@Test
void testIsFileReadyForProcessing_OldFile() throws IOException {
// Create a test file
Path testFile = tempDir.resolve("test-file.txt");
Files.write(testFile, "test content".getBytes());
// Set modified time to 10 seconds ago
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
// File should be ready for processing as it was modified more than 5 seconds ago
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
}
@Test
void testIsFileReadyForProcessing_RecentFile() throws IOException {
// Create a test file
Path testFile = tempDir.resolve("recent-file.txt");
Files.write(testFile, "test content".getBytes());
// Set modified time to just now
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now()));
// File should not be ready for processing as it was just modified
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
}
@Test
void testIsFileReadyForProcessing_NonExistentFile() {
// Create a path to a file that doesn't exist
Path nonExistentFile = tempDir.resolve("non-existent-file.txt");
// Non-existent file should not be ready for processing
assertFalse(fileMonitor.isFileReadyForProcessing(nonExistentFile));
}
@Test
void testIsFileReadyForProcessing_LockedFile() throws IOException {
// Create a test file
Path testFile = tempDir.resolve("locked-file.txt");
Files.write(testFile, "test content".getBytes());
// Set modified time to 10 seconds ago to make sure it passes the time check
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
// Verify the file is considered ready when it meets the time criteria
assertTrue(
fileMonitor.isFileReadyForProcessing(testFile),
"File should be ready for processing when sufficiently old");
}
@Test
void testPathFilter() throws IOException {
// Use a simple lambda instead of a mock for better control
Predicate<Path> pdfFilter = path -> path.toString().endsWith(".pdf");
// Create a new FileMonitor with the PDF filter
FileMonitor pdfMonitor = new FileMonitor(pdfFilter, runtimePathConfig);
// Create a PDF file
Path pdfFile = tempDir.resolve("test.pdf");
Files.write(pdfFile, "pdf content".getBytes());
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.now().minusMillis(10000)));
// Create a TXT file
Path txtFile = tempDir.resolve("test.txt");
Files.write(txtFile, "text content".getBytes());
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.now().minusMillis(10000)));
// PDF file should be ready for processing
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
// Note: In the current implementation, FileMonitor.isFileReadyForProcessing()
// doesn't check file filters directly - it only checks criteria like file existence
// and modification time. The filtering is likely handled elsewhere in the workflow.
// To avoid test failures, we'll verify that the filter itself works correctly
assertFalse(pdfFilter.test(txtFile), "PDF filter should reject txt files");
assertTrue(pdfFilter.test(pdfFile), "PDF filter should accept pdf files");
}
@Test
void testIsFileReadyForProcessing_FileInUse() throws IOException {
// Create a test file
Path testFile = tempDir.resolve("in-use-file.txt");
Files.write(testFile, "initial content".getBytes());
// Set modified time to 10 seconds ago
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
// First check that the file is ready when meeting time criteria
assertTrue(
fileMonitor.isFileReadyForProcessing(testFile),
"File should be ready for processing when sufficiently old");
// After modifying the file to simulate closing, it should still be ready
Files.write(testFile, "updated content".getBytes());
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
assertTrue(
fileMonitor.isFileReadyForProcessing(testFile),
"File should be ready for processing after updating");
}
@Test
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
// Create a test file
Path testFile = tempDir.resolve("absolute-path-file.txt");
Files.write(testFile, "test content".getBytes());
// Set modified time to 10 seconds ago
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
// File should be ready for processing as it was modified more than 5 seconds ago
// Use the absolute path to make sure it's handled correctly
assertTrue(fileMonitor.isFileReadyForProcessing(testFile.toAbsolutePath()));
}
@Test
void testIsFileReadyForProcessing_DirectoryInsteadOfFile() throws IOException {
// Create a test directory
Path testDir = tempDir.resolve("test-directory");
Files.createDirectory(testDir);
// Set modified time to 10 seconds ago
Files.setLastModifiedTime(testDir, FileTime.from(Instant.now().minusMillis(10000)));
// A directory should not be considered ready for processing
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
assertFalse(isReady, "A directory should not be considered ready for processing");
}
}

View File

@@ -0,0 +1,78 @@
package stirling.software.common.util;
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 java.io.IOException;
import org.junit.jupiter.api.Test;
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
public class FileToPdfTest {
/**
* Test the HTML to PDF conversion. This test expects an IOException when an empty HTML input is
* provided.
*/
@Test
public void testConvertHtmlToPdf() {
HTMLToPdfRequest request = new HTMLToPdfRequest();
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
String fileName = "test.html"; // Sample file name indicating an HTML file
boolean disableSanitize = false; // Flag to control sanitization
// Expect an IOException to be thrown due to empty input
Throwable thrown =
assertThrows(
IOException.class,
() ->
FileToPdf.convertHtmlToPdf(
"/path/", request, fileBytes, fileName, disableSanitize));
assertNotNull(thrown);
}
/**
* Test sanitizeZipFilename with null or empty input. It should return an empty string in these
* cases.
*/
@Test
public void testSanitizeZipFilename_NullOrEmpty() {
assertEquals("", FileToPdf.sanitizeZipFilename(null));
assertEquals("", FileToPdf.sanitizeZipFilename(" "));
}
/**
* Test sanitizeZipFilename to ensure it removes path traversal sequences. This includes
* removing both forward and backward slash sequences.
*/
@Test
public void testSanitizeZipFilename_RemovesTraversalSequences() {
String input = "../some/../path/..\\to\\file.txt";
String expected = "some/path/to/file.txt";
// Expect that the method replaces backslashes with forward slashes
// and removes path traversal sequences
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
}
/** Test sanitizeZipFilename to ensure that it removes leading drive letters and slashes. */
@Test
public void testSanitizeZipFilename_RemovesLeadingDriveAndSlashes() {
String input = "C:\\folder\\file.txt";
String expected = "folder/file.txt";
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
input = "/folder/file.txt";
expected = "folder/file.txt";
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
}
/** Test sanitizeZipFilename to verify that safe filenames remain unchanged. */
@Test
public void testSanitizeZipFilename_NoChangeForSafeNames() {
String input = "folder/subfolder/file.txt";
assertEquals(input, FileToPdf.sanitizeZipFilename(input));
}
}

View File

@@ -0,0 +1,41 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class GeneralUtilsAdditionalTest {
@Test
void testConvertSizeToBytes() {
assertEquals(1024L, GeneralUtils.convertSizeToBytes("1KB"));
assertEquals(1024L * 1024, GeneralUtils.convertSizeToBytes("1MB"));
assertEquals(1024L * 1024 * 1024, GeneralUtils.convertSizeToBytes("1GB"));
assertEquals(100L * 1024 * 1024, GeneralUtils.convertSizeToBytes("100"));
assertNull(GeneralUtils.convertSizeToBytes("invalid"));
assertNull(GeneralUtils.convertSizeToBytes(null));
}
@Test
void testFormatBytes() {
assertEquals("512 B", GeneralUtils.formatBytes(512));
assertEquals("1.00 KB", GeneralUtils.formatBytes(1024));
assertEquals("1.00 MB", GeneralUtils.formatBytes(1024L * 1024));
assertEquals("1.00 GB", GeneralUtils.formatBytes(1024L * 1024 * 1024));
}
@Test
void testURLHelpersAndUUID() {
assertTrue(GeneralUtils.isValidURL("https://example.com"));
assertFalse(GeneralUtils.isValidURL("htp:/bad"));
assertFalse(GeneralUtils.isURLReachable("http://localhost"));
assertFalse(GeneralUtils.isURLReachable("ftp://example.com"));
assertTrue(GeneralUtils.isValidUUID("123e4567-e89b-12d3-a456-426614174000"));
assertFalse(GeneralUtils.isValidUUID("not-a-uuid"));
assertFalse(GeneralUtils.isVersionHigher(null, "1.0"));
assertTrue(GeneralUtils.isVersionHigher("2.0", "1.9"));
assertFalse(GeneralUtils.isVersionHigher("1.0", "1.0.1"));
}
}

View File

@@ -0,0 +1,157 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
public class GeneralUtilsTest {
@Test
void testParsePageListWithAll() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"all"}, 5, false);
assertEquals(List.of(0, 1, 2, 3, 4), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithAllOneBased() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"all"}, 5, true);
assertEquals(List.of(1, 2, 3, 4, 5), result, "'All' keyword should return all pages.");
}
@Test
void nFunc() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"n"}, 5, true);
assertEquals(List.of(1, 2, 3, 4, 5), result, "'n' keyword should return all pages.");
}
@Test
void nFuncAdvanced() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n"}, 9, true);
// skip 0 as not valid
assertEquals(List.of(4, 8), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvancedZero() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n"}, 9, false);
// skip 0 as not valid
assertEquals(List.of(3, 7), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvanced2() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n-1"}, 9, true);
// skip -1 as not valid
assertEquals(List.of(3, 7), result, "4n-1 should do (0-1), (4-1), (8-1)");
}
@Test
void nFuncAdvanced3() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n+1"}, 9, true);
assertEquals(List.of(5, 9), result, "'All' keyword should return all pages.");
}
@Test
void nFunc_spaces() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"n + 1"}, 9, true);
assertEquals(List.of(2, 3, 4, 5, 6, 7, 8, 9), result);
}
@Test
void nFunc_consecutive_Ns_nnn() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"nnn"}, 9, true);
assertEquals(List.of(1, 8), result);
}
@Test
void nFunc_consecutive_Ns_nn() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"nn"}, 9, true);
assertEquals(List.of(1, 4, 9), result);
}
@Test
void nFunc_opening_closing_round_brackets() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)(n-2)"}, 9, true);
assertEquals(List.of(2, 6), result);
}
@Test
void nFunc_opening_round_brackets() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"2(n-1)"}, 9, true);
assertEquals(List.of(2, 4, 6, 8), result);
}
@Test
void nFunc_opening_round_brackets_n() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"n(n-1)"}, 9, true);
assertEquals(List.of(2, 6), result);
}
@Test
void nFunc_closing_round_brackets() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)2"}, 9, true);
assertEquals(List.of(2, 4, 6, 8), result);
}
@Test
void nFunc_closing_round_brackets_n() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)n"}, 9, true);
assertEquals(List.of(2, 6), result);
}
@Test
void nFunc_function_surrounded_with_brackets() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)"}, 9, true);
assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8), result);
}
@Test
void nFuncAdvanced4() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"3+2n"}, 9, true);
assertEquals(List.of(5, 7, 9), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvancedZerobased() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n"}, 9, false);
assertEquals(List.of(3, 7), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvanced2Zerobased() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n-1"}, 9, false);
assertEquals(List.of(2, 6), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithRangeOneBasedOutput() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1-3"}, 5, true);
assertEquals(List.of(1, 2, 3), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeZeroBaseOutput() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1-3"}, 5, false);
assertEquals(List.of(0, 1, 2), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeOneBasedOutputFull() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, true);
assertEquals(List.of(1, 3, 7, 8), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeOneBasedOutputFullOutOfRange() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 5, true);
assertEquals(List.of(1, 3), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeZeroBaseOutputFull() {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, false);
assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly.");
}
}

View File

@@ -0,0 +1,82 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.awt.*;
import java.awt.image.BufferedImage;
import org.junit.jupiter.api.Test;
public class ImageProcessingUtilsTest {
@Test
void testConvertColorTypeToGreyscale() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage =
ImageProcessingUtils.convertColorType(sourceImage, "greyscale");
assertNotNull(convertedImage);
assertEquals(BufferedImage.TYPE_BYTE_GRAY, convertedImage.getType());
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
// Check if a pixel is correctly converted to greyscale
Color grey = new Color(convertedImage.getRGB(0, 0));
assertEquals(grey.getRed(), grey.getGreen());
assertEquals(grey.getGreen(), grey.getBlue());
}
@Test
void testConvertColorTypeToBlackWhite() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage =
ImageProcessingUtils.convertColorType(sourceImage, "blackwhite");
assertNotNull(convertedImage);
assertEquals(BufferedImage.TYPE_BYTE_BINARY, convertedImage.getType());
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
// Check if a pixel is converted correctly (binary image will be either black or white)
int rgb = convertedImage.getRGB(0, 0);
assertTrue(rgb == Color.BLACK.getRGB() || rgb == Color.WHITE.getRGB());
}
@Test
void testConvertColorTypeToFullColor() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage =
ImageProcessingUtils.convertColorType(sourceImage, "fullcolor");
assertNotNull(convertedImage);
assertEquals(sourceImage, convertedImage);
}
@Test
void testConvertColorTypeInvalid() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage =
ImageProcessingUtils.convertColorType(sourceImage, "invalidtype");
assertNotNull(convertedImage);
assertEquals(sourceImage, convertedImage);
}
private void fillImageWithColor(BufferedImage image, Color color) {
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
image.setRGB(x, y, color.getRGB());
}
}
}
}

View File

@@ -0,0 +1,578 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.ZipSecurity;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
/**
* Tests for PDFToFile utility class. This includes both invalid content type cases and positive
* test cases that mock external process execution.
*/
@ExtendWith(MockitoExtension.class)
class PDFToFileTest {
@TempDir Path tempDir;
private PDFToFile pdfToFile;
@Mock private ProcessExecutor mockProcessExecutor;
@Mock private ProcessExecutorResult mockExecutorResult;
@BeforeEach
void setUp() {
pdfToFile = new PDFToFile();
}
@Test
void testProcessPdfToMarkdown_InvalidContentType() throws IOException, InterruptedException {
// Prepare
MultipartFile nonPdfFile =
new MockMultipartFile(
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
// Execute
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(nonPdfFile);
// Verify
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@Test
void testProcessPdfToHtml_InvalidContentType() throws IOException, InterruptedException {
// Prepare
MultipartFile nonPdfFile =
new MockMultipartFile(
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
// Execute
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(nonPdfFile);
// Verify
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@Test
void testProcessPdfToOfficeFormat_InvalidContentType()
throws IOException, InterruptedException {
// Prepare
MultipartFile nonPdfFile =
new MockMultipartFile(
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
// Execute
ResponseEntity<byte[]> response =
pdfToFile.processPdfToOfficeFormat(nonPdfFile, "docx", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@Test
void testProcessPdfToOfficeFormat_InvalidOutputFormat()
throws IOException, InterruptedException {
// Prepare
MultipartFile pdfFile =
new MockMultipartFile(
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
// Execute with invalid format
ResponseEntity<byte[]> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "invalid_format", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@Test
void testProcessPdfToMarkdown_SingleOutputFile() throws IOException, InterruptedException {
// Setup mock objects and temp files
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
mockStatic(ProcessExecutor.class)) {
// Create a mock PDF file
MultipartFile pdfFile =
new MockMultipartFile(
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
// Create a mock HTML output file
Path htmlOutputFile = tempDir.resolve("test.html");
Files.write(
htmlOutputFile,
"<html><body><h1>Test</h1><p>This is a test.</p></body></html>".getBytes());
// Setup ProcessExecutor mock
mockedStaticProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML))
.thenReturn(mockProcessExecutor);
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class), any(File.class)))
.thenAnswer(
invocation -> {
// When command is executed, simulate creation of output files
File outputDir = invocation.getArgument(1);
// Copy the mock HTML file to the output directory
Files.copy(
htmlOutputFile, Path.of(outputDir.getPath(), "test.html"));
return mockExecutorResult;
});
// Execute the method
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(pdfFile);
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
assertTrue(
response.getHeaders().getContentDisposition().toString().contains("test.md"));
}
}
@Test
void testProcessPdfToMarkdown_MultipleOutputFiles() throws IOException, InterruptedException {
// Setup mock objects and temp files
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
mockStatic(ProcessExecutor.class)) {
// Create a mock PDF file
MultipartFile pdfFile =
new MockMultipartFile(
"file",
"multipage.pdf",
"application/pdf",
"Fake PDF content".getBytes());
// Setup ProcessExecutor mock
mockedStaticProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML))
.thenReturn(mockProcessExecutor);
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class), any(File.class)))
.thenAnswer(
invocation -> {
// When command is executed, simulate creation of output files
File outputDir = invocation.getArgument(1);
// Create multiple HTML files and an image
Files.write(
Path.of(outputDir.getPath(), "multipage.html"),
"<html><body><h1>Cover</h1></body></html>".getBytes());
Files.write(
Path.of(outputDir.getPath(), "multipage-1.html"),
"<html><body><h1>Page 1</h1></body></html>".getBytes());
Files.write(
Path.of(outputDir.getPath(), "multipage-2.html"),
"<html><body><h1>Page 2</h1></body></html>".getBytes());
Files.write(
Path.of(outputDir.getPath(), "image1.png"),
"Fake image data".getBytes());
return mockExecutorResult;
});
// Execute the method
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(pdfFile);
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
// Verify content disposition indicates a zip file
assertTrue(
response.getHeaders()
.getContentDisposition()
.toString()
.contains("ToMarkdown.zip"));
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry;
boolean foundMdFiles = false;
boolean foundImage = false;
while ((entry = zipStream.getNextEntry()) != null) {
if (entry.getName().endsWith(".md")) {
foundMdFiles = true;
} else if (entry.getName().endsWith(".png")) {
foundImage = true;
}
zipStream.closeEntry();
}
assertTrue(foundMdFiles, "ZIP should contain Markdown files");
assertTrue(foundImage, "ZIP should contain image files");
}
}
}
@Test
void testProcessPdfToHtml() throws IOException, InterruptedException {
// Setup mock objects and temp files
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
mockStatic(ProcessExecutor.class)) {
// Create a mock PDF file
MultipartFile pdfFile =
new MockMultipartFile(
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
// Setup ProcessExecutor mock
mockedStaticProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML))
.thenReturn(mockProcessExecutor);
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class), any(File.class)))
.thenAnswer(
invocation -> {
// When command is executed, simulate creation of output files
File outputDir = invocation.getArgument(1);
// Create HTML files and assets
Files.write(
Path.of(outputDir.getPath(), "test.html"),
"<html><frameset></frameset></html>".getBytes());
Files.write(
Path.of(outputDir.getPath(), "test_ind.html"),
"<html><body>Index</body></html>".getBytes());
Files.write(
Path.of(outputDir.getPath(), "test_img.png"),
"Fake image data".getBytes());
return mockExecutorResult;
});
// Execute the method
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(pdfFile);
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
// Verify content disposition indicates a zip file
assertTrue(
response.getHeaders()
.getContentDisposition()
.toString()
.contains("testToHtml.zip"));
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry;
boolean foundMainHtml = false;
boolean foundIndexHtml = false;
boolean foundImage = false;
while ((entry = zipStream.getNextEntry()) != null) {
if ("test.html".equals(entry.getName())) {
foundMainHtml = true;
} else if ("test_ind.html".equals(entry.getName())) {
foundIndexHtml = true;
} else if ("test_img.png".equals(entry.getName())) {
foundImage = true;
}
zipStream.closeEntry();
}
assertTrue(foundMainHtml, "ZIP should contain main HTML file");
assertTrue(foundIndexHtml, "ZIP should contain index HTML file");
assertTrue(foundImage, "ZIP should contain image files");
}
}
}
@Test
void testProcessPdfToOfficeFormat_SingleOutputFile() throws IOException, InterruptedException {
// Setup mock objects and temp files
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
mockStatic(ProcessExecutor.class)) {
// Create a mock PDF file
MultipartFile pdfFile =
new MockMultipartFile(
"file",
"document.pdf",
"application/pdf",
"Fake PDF content".getBytes());
// Setup ProcessExecutor mock
mockedStaticProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
.thenReturn(mockProcessExecutor);
when(mockProcessExecutor.runCommandWithOutputHandling(
argThat(
args ->
args.contains("--convert-to")
&& args.contains("docx"))))
.thenAnswer(
invocation -> {
// When command is executed, find the output directory argument
List<String> args = invocation.getArgument(0);
String outDir = null;
for (int i = 0; i < args.size(); i++) {
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
outDir = args.get(i + 1);
break;
}
}
// Create output file
Files.write(
Path.of(outDir, "document.docx"),
"Fake DOCX content".getBytes());
return mockExecutorResult;
});
// Execute the method with docx format
ResponseEntity<byte[]> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
// Verify content disposition has correct filename
assertTrue(
response.getHeaders()
.getContentDisposition()
.toString()
.contains("document.docx"));
}
}
@Test
void testProcessPdfToOfficeFormat_MultipleOutputFiles()
throws IOException, InterruptedException {
// Setup mock objects and temp files
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
mockStatic(ProcessExecutor.class)) {
// Create a mock PDF file
MultipartFile pdfFile =
new MockMultipartFile(
"file",
"document.pdf",
"application/pdf",
"Fake PDF content".getBytes());
// Setup ProcessExecutor mock
mockedStaticProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
.thenReturn(mockProcessExecutor);
when(mockProcessExecutor.runCommandWithOutputHandling(
argThat(args -> args.contains("--convert-to") && args.contains("odp"))))
.thenAnswer(
invocation -> {
// When command is executed, find the output directory argument
List<String> args = invocation.getArgument(0);
String outDir = null;
for (int i = 0; i < args.size(); i++) {
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
outDir = args.get(i + 1);
break;
}
}
// Create multiple output files (simulating a presentation with
// multiple files)
Files.write(
Path.of(outDir, "document.odp"),
"Fake ODP content".getBytes());
Files.write(
Path.of(outDir, "document_media1.png"),
"Image 1 content".getBytes());
Files.write(
Path.of(outDir, "document_media2.png"),
"Image 2 content".getBytes());
return mockExecutorResult;
});
// Execute the method with ODP format
ResponseEntity<byte[]> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "odp", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
// Verify content disposition for zip file
assertTrue(
response.getHeaders()
.getContentDisposition()
.toString()
.contains("documentToodp.zip"));
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry;
boolean foundMainFile = false;
boolean foundMediaFiles = false;
while ((entry = zipStream.getNextEntry()) != null) {
if ("document.odp".equals(entry.getName())) {
foundMainFile = true;
} else if (entry.getName().startsWith("document_media")) {
foundMediaFiles = true;
}
zipStream.closeEntry();
}
assertTrue(foundMainFile, "ZIP should contain main ODP file");
assertTrue(foundMediaFiles, "ZIP should contain media files");
}
}
}
@Test
void testProcessPdfToOfficeFormat_TextFormat() throws IOException, InterruptedException {
// Setup mock objects and temp files
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
mockStatic(ProcessExecutor.class)) {
// Create a mock PDF file
MultipartFile pdfFile =
new MockMultipartFile(
"file",
"document.pdf",
"application/pdf",
"Fake PDF content".getBytes());
// Setup ProcessExecutor mock
mockedStaticProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
.thenReturn(mockProcessExecutor);
when(mockProcessExecutor.runCommandWithOutputHandling(
argThat(
args ->
args.contains("--convert-to")
&& args.contains("txt:Text"))))
.thenAnswer(
invocation -> {
// When command is executed, find the output directory argument
List<String> args = invocation.getArgument(0);
String outDir = null;
for (int i = 0; i < args.size(); i++) {
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
outDir = args.get(i + 1);
break;
}
}
// Create text output file
Files.write(
Path.of(outDir, "document.txt"),
"Extracted text content".getBytes());
return mockExecutorResult;
});
// Execute the method with text format
ResponseEntity<byte[]> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "txt:Text", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
// Verify content disposition has txt extension
assertTrue(
response.getHeaders()
.getContentDisposition()
.toString()
.contains("document.txt"));
}
}
@Test
void testProcessPdfToOfficeFormat_NoFilename() throws IOException, InterruptedException {
// Setup mock objects and temp files
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
mockStatic(ProcessExecutor.class)) {
// Create a mock PDF file with no filename
MultipartFile pdfFile =
new MockMultipartFile(
"file", "", "application/pdf", "Fake PDF content".getBytes());
// Setup ProcessExecutor mock
mockedStaticProcessExecutor
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
.thenReturn(mockProcessExecutor);
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class)))
.thenAnswer(
invocation -> {
// When command is executed, find the output directory argument
List<String> args = invocation.getArgument(0);
String outDir = null;
for (int i = 0; i < args.size(); i++) {
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
outDir = args.get(i + 1);
break;
}
}
// Create output file - uses default name
Files.write(
Path.of(outDir, "output.docx"),
"Fake DOCX content".getBytes());
return mockExecutorResult;
});
// Execute the method
ResponseEntity<byte[]> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
// Verify content disposition contains output.docx
assertTrue(
response.getHeaders()
.getContentDisposition()
.toString()
.contains("output.docx"));
}
}
}

View File

@@ -0,0 +1,125 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
public class PdfUtilsTest {
@Test
void testTextToPageSize() {
assertEquals(PDRectangle.A4, PdfUtils.textToPageSize("A4"));
assertEquals(PDRectangle.LETTER, PdfUtils.textToPageSize("LETTER"));
assertThrows(IllegalArgumentException.class, () -> PdfUtils.textToPageSize("INVALID"));
}
@Test
void testHasImagesOnPage() throws IOException {
// Mock a PDPage and its resources
PDPage page = Mockito.mock(PDPage.class);
PDResources resources = Mockito.mock(PDResources.class);
Mockito.when(page.getResources()).thenReturn(resources);
// Case 1: No images in resources
Mockito.when(resources.getXObjectNames()).thenReturn(Collections.emptySet());
assertFalse(PdfUtils.hasImagesOnPage(page));
// Case 2: Resources with an image
Set<COSName> xObjectNames = new HashSet<>();
COSName cosName = Mockito.mock(COSName.class);
xObjectNames.add(cosName);
PDImageXObject imageXObject = Mockito.mock(PDImageXObject.class);
Mockito.when(resources.getXObjectNames()).thenReturn(xObjectNames);
Mockito.when(resources.getXObject(cosName)).thenReturn(imageXObject);
assertTrue(PdfUtils.hasImagesOnPage(page));
}
@Test
void testPageCountComparators() throws Exception {
PDDocument doc1 = new PDDocument();
doc1.addPage(new PDPage());
doc1.addPage(new PDPage());
doc1.addPage(new PDPage());
PdfUtils utils = new PdfUtils();
assertTrue(utils.pageCount(doc1, 2, "greater"));
PDDocument doc2 = new PDDocument();
doc2.addPage(new PDPage());
doc2.addPage(new PDPage());
doc2.addPage(new PDPage());
assertTrue(utils.pageCount(doc2, 3, "equal"));
PDDocument doc3 = new PDDocument();
doc3.addPage(new PDPage());
doc3.addPage(new PDPage());
assertTrue(utils.pageCount(doc3, 5, "less"));
PDDocument doc4 = new PDDocument();
doc4.addPage(new PDPage());
assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad"));
}
@Test
void testPageSize() throws Exception {
PDDocument doc = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
doc.addPage(page);
PDRectangle rect = page.getMediaBox();
String expected = rect.getWidth() + "x" + rect.getHeight();
PdfUtils utils = new PdfUtils();
assertTrue(utils.pageSize(doc, expected));
}
@Test
void testOverlayImage() throws Exception {
PDDocument doc = new PDDocument();
doc.addPage(new PDPage(PDRectangle.A4));
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
doc.save(pdfOut);
doc.close();
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.RED);
g.fillRect(0, 0, 10, 10);
g.dispose();
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
javax.imageio.ImageIO.write(image, "png", imgOut);
PdfMetadataService meta =
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
byte[] result =
PdfUtils.overlayImage(
factory, pdfOut.toByteArray(), imgOut.toByteArray(), 0, 0, false);
try (PDDocument resultDoc = factory.load(result)) {
assertEquals(1, resultDoc.getNumberOfPages());
}
}
}

View File

@@ -0,0 +1,62 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ProcessExecutorTest {
private ProcessExecutor processExecutor;
@BeforeEach
public void setUp() {
// Initialize the ProcessExecutor instance
processExecutor = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE);
}
@Test
public void testRunCommandWithOutputHandling() throws IOException, InterruptedException {
// Mock the command to execute
List<String> command = new ArrayList<>();
command.add("java");
command.add("-version");
// Execute the command
ProcessExecutor.ProcessExecutorResult result =
processExecutor.runCommandWithOutputHandling(command);
// Check the exit code and output messages
assertEquals(0, result.getRc());
assertNotNull(result.getMessages()); // Check if messages are not null
}
@Test
public void testRunCommandWithOutputHandling_Error() {
// Mock the command to execute
List<String> command = new ArrayList<>();
command.add("nonexistent-command");
// Execute the command and expect an IOException
IOException thrown =
assertThrows(
IOException.class,
() -> {
processExecutor.runCommandWithOutputHandling(command);
});
// Check the exception message to ensure it indicates the command was not found
String errorMessage = thrown.getMessage();
assertTrue(
errorMessage.contains("error=2")
|| errorMessage.contains("No such file or directory"),
"Unexpected error message: " + errorMessage);
}
}

View File

@@ -0,0 +1,69 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
public class PropertyConfigsTest {
@Test
public void testGetBooleanValue_WithKeys() {
// Define keys and default value
List<String> keys = Arrays.asList("test.key1", "test.key2", "test.key3");
boolean defaultValue = false;
// Set property for one of the keys
System.setProperty("test.key2", "true");
// Call the method under test
boolean result = PropertyConfigs.getBooleanValue(keys, defaultValue);
// Verify the result
assertEquals(true, result);
}
@Test
public void testGetStringValue_WithKeys() {
// Define keys and default value
List<String> keys = Arrays.asList("test.key1", "test.key2", "test.key3");
String defaultValue = "default";
// Set property for one of the keys
System.setProperty("test.key2", "value");
// Call the method under test
String result = PropertyConfigs.getStringValue(keys, defaultValue);
// Verify the result
assertEquals("value", result);
}
@Test
public void testGetBooleanValue_WithKey() {
// Define key and default value
String key = "test.key";
boolean defaultValue = true;
// Call the method under test
boolean result = PropertyConfigs.getBooleanValue(key, defaultValue);
// Verify the result
assertEquals(true, result);
}
@Test
public void testGetStringValue_WithKey() {
// Define key and default value
String key = "test.key";
String defaultValue = "default";
// Call the method under test
String result = PropertyConfigs.getStringValue(key, defaultValue);
// Verify the result
assertEquals("default", result);
}
}

View File

@@ -0,0 +1,46 @@
package stirling.software.common.util;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
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.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.common.model.enumeration.UsernameAttribute;
import stirling.software.common.model.oauth2.GitHubProvider;
import stirling.software.common.model.oauth2.GoogleProvider;
import stirling.software.common.model.oauth2.Provider;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ProviderUtilsTest {
@Test
void testSuccessfulValidation() {
var provider = mock(GitHubProvider.class);
when(provider.getClientId()).thenReturn("clientId");
when(provider.getClientSecret()).thenReturn("clientSecret");
when(provider.getScopes()).thenReturn(List.of("read:user"));
Assertions.assertTrue(ProviderUtils.validateProvider(provider));
}
@ParameterizedTest
@MethodSource("providerParams")
void testUnsuccessfulValidation(Provider provider) {
Assertions.assertFalse(ProviderUtils.validateProvider(provider));
}
public static Stream<Arguments> providerParams() {
Provider generic = null;
var google =
new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN);
return Stream.of(Arguments.of(generic), Arguments.of(google), Arguments.of(github));
}
}

View File

@@ -0,0 +1,311 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class RequestUriUtilsTest {
@Test
void testIsStaticResource() {
// Test static resources without context path
assertTrue(
RequestUriUtils.isStaticResource("/css/styles.css"), "CSS files should be static");
assertTrue(RequestUriUtils.isStaticResource("/js/script.js"), "JS files should be static");
assertTrue(
RequestUriUtils.isStaticResource("/images/logo.png"),
"Image files should be static");
assertTrue(
RequestUriUtils.isStaticResource("/public/index.html"),
"Public files should be static");
assertTrue(
RequestUriUtils.isStaticResource("/pdfjs/pdf.worker.js"),
"PDF.js files should be static");
assertTrue(
RequestUriUtils.isStaticResource("/api/v1/info/status"),
"API status should be static");
assertTrue(
RequestUriUtils.isStaticResource("/some-path/icon.svg"),
"SVG files should be static");
assertTrue(RequestUriUtils.isStaticResource("/login"), "Login page should be static");
assertTrue(RequestUriUtils.isStaticResource("/error"), "Error page should be static");
// Test non-static resources
assertFalse(
RequestUriUtils.isStaticResource("/api/v1/users"),
"API users should not be static");
assertFalse(
RequestUriUtils.isStaticResource("/api/v1/orders"),
"API orders should not be static");
assertFalse(RequestUriUtils.isStaticResource("/"), "Root path should not be static");
assertFalse(
RequestUriUtils.isStaticResource("/register"),
"Register page should not be static");
assertFalse(
RequestUriUtils.isStaticResource("/api/v1/products"),
"API products should not be static");
}
@Test
void testIsStaticResourceWithContextPath() {
String contextPath = "/myapp";
// Test static resources with context path
assertTrue(
RequestUriUtils.isStaticResource(contextPath, contextPath + "/css/styles.css"),
"CSS with context path should be static");
assertTrue(
RequestUriUtils.isStaticResource(contextPath, contextPath + "/js/script.js"),
"JS with context path should be static");
assertTrue(
RequestUriUtils.isStaticResource(contextPath, contextPath + "/images/logo.png"),
"Images with context path should be static");
assertTrue(
RequestUriUtils.isStaticResource(contextPath, contextPath + "/login"),
"Login with context path should be static");
// Test non-static resources with context path
assertFalse(
RequestUriUtils.isStaticResource(contextPath, contextPath + "/api/v1/users"),
"API users with context path should not be static");
assertFalse(
RequestUriUtils.isStaticResource(contextPath, "/"),
"Root path with context path should not be static");
}
@ParameterizedTest
@ValueSource(
strings = {
"robots.txt",
"/favicon.ico",
"/icon.svg",
"/image.png",
"/site.webmanifest",
"/app/logo.svg",
"/downloads/document.png",
"/assets/brand.ico",
"/any/path/with/image.svg",
"/deep/nested/folder/icon.png"
})
void testIsStaticResourceWithFileExtensions(String path) {
assertTrue(
RequestUriUtils.isStaticResource(path),
"Files with specific extensions should be static regardless of path");
}
@Test
void testIsTrackableResource() {
// Test non-trackable resources (returns false)
assertFalse(
RequestUriUtils.isTrackableResource("/js/script.js"),
"JS files should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/v1/api-docs"),
"API docs should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("robots.txt"),
"robots.txt should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/images/logo.png"),
"Images should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/styles.css"),
"CSS files should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/script.js.map"),
"Map files should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/icon.svg"),
"SVG files should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/popularity.txt"),
"Popularity file should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/script.js"),
"JS files should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/swagger/index.html"),
"Swagger files should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/api/v1/info/status"),
"API info should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/site.webmanifest"),
"Webmanifest should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/fonts/font.woff"),
"Fonts should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/pdfjs/viewer.js"),
"PDF.js files should not be trackable");
// Test trackable resources (returns true)
assertTrue(RequestUriUtils.isTrackableResource("/login"), "Login page should be trackable");
assertTrue(
RequestUriUtils.isTrackableResource("/register"),
"Register page should be trackable");
assertTrue(
RequestUriUtils.isTrackableResource("/api/v1/users"),
"API users should be trackable");
assertTrue(RequestUriUtils.isTrackableResource("/"), "Root path should be trackable");
assertTrue(
RequestUriUtils.isTrackableResource("/some-other-path"),
"Other paths should be trackable");
}
@Test
void testIsTrackableResourceWithContextPath() {
String contextPath = "/myapp";
// Test with context path
assertFalse(
RequestUriUtils.isTrackableResource(contextPath, "/js/script.js"),
"JS files should not be trackable with context path");
assertTrue(
RequestUriUtils.isTrackableResource(contextPath, "/login"),
"Login page should be trackable with context path");
// Additional tests with context path
assertFalse(
RequestUriUtils.isTrackableResource(contextPath, "/fonts/custom.woff"),
"Font files should not be trackable with context path");
assertFalse(
RequestUriUtils.isTrackableResource(contextPath, "/images/header.png"),
"Images should not be trackable with context path");
assertFalse(
RequestUriUtils.isTrackableResource(contextPath, "/swagger/ui.html"),
"Swagger UI should not be trackable with context path");
assertTrue(
RequestUriUtils.isTrackableResource(contextPath, "/account/profile"),
"Account page should be trackable with context path");
assertTrue(
RequestUriUtils.isTrackableResource(contextPath, "/pdf/view"),
"PDF view page should be trackable with context path");
}
@ParameterizedTest
@ValueSource(
strings = {
"/js/util.js",
"/v1/api-docs/swagger.json",
"/robots.txt",
"/images/header/logo.png",
"/styles/theme.css",
"/build/app.js.map",
"/assets/icon.svg",
"/data/popularity.txt",
"/bundle.js",
"/api/swagger-ui.html",
"/api/v1/info/health",
"/site.webmanifest",
"/fonts/roboto.woff",
"/pdfjs/viewer.js"
})
void testNonTrackableResources(String path) {
assertFalse(
RequestUriUtils.isTrackableResource(path),
"Resources matching patterns should not be trackable: " + path);
}
@ParameterizedTest
@ValueSource(
strings = {
"/",
"/home",
"/login",
"/register",
"/pdf/merge",
"/pdf/split",
"/api/v1/users/1",
"/api/v1/documents/process",
"/settings",
"/account/profile",
"/dashboard",
"/help",
"/about"
})
void testTrackableResources(String path) {
assertTrue(
RequestUriUtils.isTrackableResource(path),
"App routes should be trackable: " + path);
}
@Test
void testEdgeCases() {
// Test with empty strings
assertFalse(RequestUriUtils.isStaticResource("", ""), "Empty path should not be static");
assertTrue(RequestUriUtils.isTrackableResource("", ""), "Empty path should be trackable");
// Test with null-like behavior (would actually throw NPE in real code)
// These are not actual null tests but shows handling of odd cases
assertFalse(RequestUriUtils.isStaticResource("null"), "String 'null' should not be static");
// Test String "null" as a path
boolean isTrackable = RequestUriUtils.isTrackableResource("null");
assertTrue(isTrackable, "String 'null' should be trackable");
// Mixed case extensions test - note that Java's endsWith() is case-sensitive
// We'll check actual behavior and document it rather than asserting
// Always test the lowercase versions which should definitely work
assertTrue(
RequestUriUtils.isStaticResource("/logo.png"), "PNG (lowercase) should be static");
assertTrue(
RequestUriUtils.isStaticResource("/icon.svg"), "SVG (lowercase) should be static");
// Path with query parameters
assertFalse(
RequestUriUtils.isStaticResource("/api/users?page=1"),
"Path with query params should respect base path");
assertTrue(
RequestUriUtils.isStaticResource("/images/logo.png?v=123"),
"Static resource with query params should still be static");
// Paths with fragments
assertTrue(
RequestUriUtils.isStaticResource("/css/styles.css#section1"),
"CSS with fragment should be static");
// Multiple dots in filename
assertTrue(
RequestUriUtils.isStaticResource("/js/jquery.min.js"),
"JS with multiple dots should be static");
// Special characters in path
assertTrue(
RequestUriUtils.isStaticResource("/images/user's-photo.png"),
"Path with special chars should be handled correctly");
}
@Test
void testComplexPaths() {
// Test complex static resource paths
assertTrue(
RequestUriUtils.isStaticResource("/css/theme/dark/styles.css"),
"Nested CSS should be static");
assertTrue(
RequestUriUtils.isStaticResource("/fonts/open-sans/bold/font.woff"),
"Nested font should be static");
assertTrue(
RequestUriUtils.isStaticResource("/js/vendor/jquery/3.5.1/jquery.min.js"),
"Versioned JS should be static");
// Test complex paths with context
String contextPath = "/app";
assertTrue(
RequestUriUtils.isStaticResource(
contextPath, contextPath + "/css/theme/dark/styles.css"),
"Nested CSS with context should be static");
// Test boundary cases for isTrackableResource
assertFalse(
RequestUriUtils.isTrackableResource("/js-framework/components"),
"Path starting with js- should not be treated as JS resource");
assertFalse(
RequestUriUtils.isTrackableResource("/fonts-selection"),
"Path starting with fonts- should not be treated as font resource");
}
}

View File

@@ -0,0 +1,345 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Toolkit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
class UIScalingTest {
private MockedStatic<Toolkit> mockedToolkit;
private Toolkit mockedDefaultToolkit;
@BeforeEach
void setUp() {
// Set up mocking of Toolkit
mockedToolkit = mockStatic(Toolkit.class);
mockedDefaultToolkit = Mockito.mock(Toolkit.class);
// Return mocked toolkit when Toolkit.getDefaultToolkit() is called
mockedToolkit.when(Toolkit::getDefaultToolkit).thenReturn(mockedDefaultToolkit);
}
@AfterEach
void tearDown() {
if (mockedToolkit != null) {
mockedToolkit.close();
}
}
@Test
void testGetWidthScaleFactor() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
double scaleFactor = UIScaling.getWidthScaleFactor();
// Assert
assertEquals(2.0, scaleFactor, 0.001, "Scale factor should be 2.0 for 4K width");
verify(mockedDefaultToolkit, times(1)).getScreenSize();
}
@Test
void testGetHeightScaleFactor() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
double scaleFactor = UIScaling.getHeightScaleFactor();
// Assert
assertEquals(2.0, scaleFactor, 0.001, "Scale factor should be 2.0 for 4K height");
verify(mockedDefaultToolkit, times(1)).getScreenSize();
}
@Test
void testGetWidthScaleFactor_HD() {
// Arrange - HD resolution
Dimension screenSize = new Dimension(1920, 1080);
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
double scaleFactor = UIScaling.getWidthScaleFactor();
// Assert
assertEquals(1.0, scaleFactor, 0.001, "Scale factor should be 1.0 for HD width");
}
@Test
void testGetHeightScaleFactor_HD() {
// Arrange - HD resolution
Dimension screenSize = new Dimension(1920, 1080);
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
double scaleFactor = UIScaling.getHeightScaleFactor();
// Assert
assertEquals(1.0, scaleFactor, 0.001, "Scale factor should be 1.0 for HD height");
}
@Test
void testGetWidthScaleFactor_SmallScreen() {
// Arrange - Small screen
Dimension screenSize = new Dimension(1366, 768);
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
double scaleFactor = UIScaling.getWidthScaleFactor();
// Assert
assertEquals(0.711, scaleFactor, 0.001, "Scale factor should be ~0.711 for 1366x768 width");
}
@Test
void testGetHeightScaleFactor_SmallScreen() {
// Arrange - Small screen
Dimension screenSize = new Dimension(1366, 768);
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
double scaleFactor = UIScaling.getHeightScaleFactor();
// Assert
assertEquals(
0.711, scaleFactor, 0.001, "Scale factor should be ~0.711 for 1366x768 height");
}
@Test
void testScaleWidth() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
int scaledWidth = UIScaling.scaleWidth(100);
// Assert
assertEquals(200, scaledWidth, "Width should be scaled by factor of 2");
}
@Test
void testScaleHeight() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
int scaledHeight = UIScaling.scaleHeight(100);
// Assert
assertEquals(200, scaledHeight, "Height should be scaled by factor of 2");
}
@Test
void testScaleWidth_SmallScreen() {
// Arrange - Small screen
Dimension screenSize = new Dimension(960, 540); // Half of HD
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
int scaledWidth = UIScaling.scaleWidth(100);
// Assert
assertEquals(50, scaledWidth, "Width should be scaled by factor of 0.5");
}
@Test
void testScaleHeight_SmallScreen() {
// Arrange - Small screen
Dimension screenSize = new Dimension(960, 540); // Half of HD
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Act
int scaledHeight = UIScaling.scaleHeight(100);
// Assert
assertEquals(50, scaledHeight, "Height should be scaled by factor of 0.5");
}
@Test
void testScaleDimension() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
Dimension originalDim = new Dimension(200, 150);
// Act
Dimension scaledDim = UIScaling.scale(originalDim);
// Assert
assertEquals(400, scaledDim.width, "Width should be scaled by factor of 2");
assertEquals(300, scaledDim.height, "Height should be scaled by factor of 2");
// Verify the original dimension is not modified
assertEquals(200, originalDim.width, "Original width should remain unchanged");
assertEquals(150, originalDim.height, "Original height should remain unchanged");
}
@Test
void testScaleInsets() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
Insets originalInsets = new Insets(10, 20, 30, 40);
// Act
Insets scaledInsets = UIScaling.scale(originalInsets);
// Assert
assertEquals(20, scaledInsets.top, "Top inset should be scaled by factor of 2");
assertEquals(40, scaledInsets.left, "Left inset should be scaled by factor of 2");
assertEquals(60, scaledInsets.bottom, "Bottom inset should be scaled by factor of 2");
assertEquals(80, scaledInsets.right, "Right inset should be scaled by factor of 2");
// Verify the original insets are not modified
assertEquals(10, originalInsets.top, "Original top inset should remain unchanged");
assertEquals(20, originalInsets.left, "Original left inset should remain unchanged");
assertEquals(30, originalInsets.bottom, "Original bottom inset should remain unchanged");
assertEquals(40, originalInsets.right, "Original right inset should remain unchanged");
}
@Test
void testScaleFont() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
Font originalFont = new Font("Arial", Font.PLAIN, 12);
// Act
Font scaledFont = UIScaling.scaleFont(originalFont);
// Assert
assertEquals(
24.0f, scaledFont.getSize2D(), 0.001f, "Font size should be scaled by factor of 2");
// Font family might be substituted by the system, so we don't test it
assertEquals(Font.PLAIN, scaledFont.getStyle(), "Font style should remain unchanged");
}
@Test
void testScaleFont_DifferentWidthHeightScales() {
// Arrange - Different width and height scaling factors
Dimension screenSize =
new Dimension(2560, 1440); // 1.33x width, 1.33x height of base resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
Font originalFont = new Font("Arial", Font.PLAIN, 12);
// Act
Font scaledFont = UIScaling.scaleFont(originalFont);
// Assert
// Should use the smaller of the two scale factors, which is the same in this case
assertEquals(
16.0f,
scaledFont.getSize2D(),
0.001f,
"Font size should be scaled by factor of 1.33");
}
@Test
void testScaleFont_UnevenScales() {
// Arrange - different width and height scale factors
Dimension screenSize = new Dimension(3840, 1080); // 2x width, 1x height
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
Font originalFont = new Font("Arial", Font.PLAIN, 12);
// Act
Font scaledFont = UIScaling.scaleFont(originalFont);
// Assert - should use the smaller of the two scale factors (height in this case)
assertEquals(
12.0f,
scaledFont.getSize2D(),
0.001f,
"Font size should be scaled by the smaller factor (1.0)");
}
@Test
void testScaleIcon_NullIcon() {
// Act
Image result = UIScaling.scaleIcon(null, 100, 100);
// Assert
assertNull(result, "Should return null for null input");
}
@Test
void testScaleIcon_SquareImage() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Create a mock square image
Image mockImage = Mockito.mock(Image.class);
when(mockImage.getWidth(null)).thenReturn(100);
when(mockImage.getHeight(null)).thenReturn(100);
when(mockImage.getScaledInstance(anyInt(), anyInt(), anyInt())).thenReturn(mockImage);
// Act
Image result = UIScaling.scaleIcon(mockImage, 100, 100);
// Assert
assertNotNull(result, "Should return a non-null result");
verify(mockImage).getScaledInstance(eq(200), eq(200), eq(Image.SCALE_SMOOTH));
}
@Test
void testScaleIcon_WideImage() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Create a mock image with a 2:1 aspect ratio (wide)
Image mockImage = Mockito.mock(Image.class);
when(mockImage.getWidth(null)).thenReturn(200);
when(mockImage.getHeight(null)).thenReturn(100);
when(mockImage.getScaledInstance(anyInt(), anyInt(), anyInt())).thenReturn(mockImage);
// Act
Image result = UIScaling.scaleIcon(mockImage, 100, 100);
// Assert
assertNotNull(result, "Should return a non-null result");
// For a wide image (2:1), the width should be twice the height to maintain aspect ratio
verify(mockImage).getScaledInstance(anyInt(), anyInt(), eq(Image.SCALE_SMOOTH));
}
@Test
void testScaleIcon_TallImage() {
// Arrange
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
// Create a mock image with a 1:2 aspect ratio (tall)
Image mockImage = Mockito.mock(Image.class);
when(mockImage.getWidth(null)).thenReturn(100);
when(mockImage.getHeight(null)).thenReturn(200);
when(mockImage.getScaledInstance(anyInt(), anyInt(), anyInt())).thenReturn(mockImage);
// Act
Image result = UIScaling.scaleIcon(mockImage, 100, 100);
// Assert
assertNotNull(result, "Should return a non-null result");
// For a tall image (1:2), the height should be twice the width to maintain aspect ratio
verify(mockImage).getScaledInstance(anyInt(), anyInt(), eq(Image.SCALE_SMOOTH));
}
}

View File

@@ -0,0 +1,279 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.net.ServerSocket;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import jakarta.servlet.http.HttpServletRequest;
@ExtendWith(MockitoExtension.class)
class UrlUtilsTest {
@Mock private HttpServletRequest request;
@Test
void testGetOrigin() {
// Arrange
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("/myapp");
// Act
String origin = UrlUtils.getOrigin(request);
// Assert
assertEquals(
"http://localhost:8080/myapp", origin, "Origin URL should be correctly formatted");
}
@Test
void testGetOriginWithHttps() {
// Arrange
when(request.getScheme()).thenReturn("https");
when(request.getServerName()).thenReturn("example.com");
when(request.getServerPort()).thenReturn(443);
when(request.getContextPath()).thenReturn("");
// Act
String origin = UrlUtils.getOrigin(request);
// Assert
assertEquals(
"https://example.com:443",
origin,
"HTTPS origin URL should be correctly formatted");
}
@Test
void testGetOriginWithEmptyContextPath() {
// Arrange
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
// Act
String origin = UrlUtils.getOrigin(request);
// Assert
assertEquals(
"http://localhost:8080",
origin,
"Origin URL with empty context path should be correct");
}
@Test
void testGetOriginWithSpecialCharacters() {
// Arrange - Test with server name containing special characters
when(request.getScheme()).thenReturn("https");
when(request.getServerName()).thenReturn("internal-server.example-domain.com");
when(request.getServerPort()).thenReturn(8443);
when(request.getContextPath()).thenReturn("/app-v1.2");
// Act
String origin = UrlUtils.getOrigin(request);
// Assert
assertEquals(
"https://internal-server.example-domain.com:8443/app-v1.2",
origin,
"Origin URL with special characters should be correctly formatted");
}
@Test
void testGetOriginWithIPv4Address() {
// Arrange
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("192.168.1.100");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("/app");
// Act
String origin = UrlUtils.getOrigin(request);
// Assert
assertEquals(
"http://192.168.1.100:8080/app",
origin,
"Origin URL with IPv4 address should be correctly formatted");
}
@Test
void testGetOriginWithNonStandardPort() {
// Arrange
when(request.getScheme()).thenReturn("https");
when(request.getServerName()).thenReturn("example.org");
when(request.getServerPort()).thenReturn(8443);
when(request.getContextPath()).thenReturn("/api");
// Act
String origin = UrlUtils.getOrigin(request);
// Assert
assertEquals(
"https://example.org:8443/api",
origin,
"Origin URL with non-standard port should be correctly formatted");
}
@Test
void testIsPortAvailable() {
// We'll use a real server socket for this test
ServerSocket socket = null;
int port = 12345; // Choose a port unlikely to be in use
try {
// First check the port is available
boolean initialAvailability = UrlUtils.isPortAvailable(port);
// Then occupy the port
socket = new ServerSocket(port);
// Now check the port is no longer available
boolean afterSocketCreation = UrlUtils.isPortAvailable(port);
// Assert
assertTrue(initialAvailability, "Port should be available initially");
assertFalse(
afterSocketCreation, "Port should not be available after socket is created");
} catch (IOException e) {
// This might happen if the port is already in use by another process
// We'll just verify the behavior of isPortAvailable matches what we expect
assertFalse(
UrlUtils.isPortAvailable(port),
"Port should not be available if exception is thrown");
} finally {
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
// Ignore cleanup exceptions
}
}
}
}
@Test
void testFindAvailablePort() {
// We'll create a socket on a port and ensure findAvailablePort returns a different port
ServerSocket socket = null;
int startPort = 12346; // Choose a port unlikely to be in use
try {
// Occupy the start port
socket = new ServerSocket(startPort);
// Find an available port
String availablePort = UrlUtils.findAvailablePort(startPort);
// Assert the returned port is not the occupied one
assertNotEquals(
String.valueOf(startPort),
availablePort,
"findAvailablePort should not return an occupied port");
// Verify the returned port is actually available
int portNumber = Integer.parseInt(availablePort);
// Close our test socket before checking the found port
socket.close();
socket = null;
// The port should now be available
assertTrue(
UrlUtils.isPortAvailable(portNumber),
"The port returned by findAvailablePort should be available");
} catch (IOException e) {
// If we can't create the socket, skip this assertion
} finally {
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
// Ignore cleanup exceptions
}
}
}
}
@Test
void testFindAvailablePortWithAvailableStartPort() {
// Find an available port without occupying any
int startPort = 23456; // Choose a different unlikely-to-be-used port
// Make sure the port is available first
if (UrlUtils.isPortAvailable(startPort)) {
// Find an available port
String availablePort = UrlUtils.findAvailablePort(startPort);
// Assert the returned port is the start port since it's available
assertEquals(
String.valueOf(startPort),
availablePort,
"findAvailablePort should return the start port if it's available");
}
}
@Test
void testFindAvailablePortWithSequentialUsedPorts() {
// This test checks that findAvailablePort correctly skips multiple occupied ports
ServerSocket socket1 = null;
ServerSocket socket2 = null;
int startPort = 34567; // Another unlikely-to-be-used port
try {
// First verify the port is available
if (!UrlUtils.isPortAvailable(startPort)) {
return;
}
// Occupy two sequential ports
socket1 = new ServerSocket(startPort);
socket2 = new ServerSocket(startPort + 1);
// Find an available port starting from our occupied range
String availablePort = UrlUtils.findAvailablePort(startPort);
int foundPort = Integer.parseInt(availablePort);
// Should have skipped the two occupied ports
assertTrue(
foundPort >= startPort + 2,
"findAvailablePort should skip sequential occupied ports");
// Verify the found port is actually available
try (ServerSocket testSocket = new ServerSocket(foundPort)) {
assertTrue(testSocket.isBound(), "The found port should be bindable");
}
} catch (IOException e) {
// Skip test if we encounter IO exceptions
} finally {
// Clean up resources
try {
if (socket1 != null && !socket1.isClosed()) socket1.close();
if (socket2 != null && !socket2.isClosed()) socket2.close();
} catch (IOException e) {
// Ignore cleanup exceptions
}
}
}
@Test
void testIsPortAvailableWithPrivilegedPorts() {
// Skip tests for privileged ports as they typically require root access
// and results are environment-dependent
}
}

View File

@@ -0,0 +1,117 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
public class WebResponseUtilsTest {
@Test
public void testBoasToWebResponse() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("Sample PDF content".getBytes());
String docName = "sample.pdf";
ResponseEntity<byte[]> responseEntity =
WebResponseUtils.boasToWebResponse(baos, docName);
assertNotNull(responseEntity);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
assertNotNull(responseEntity.getBody());
HttpHeaders headers = responseEntity.getHeaders();
assertNotNull(headers);
assertEquals(MediaType.APPLICATION_PDF, headers.getContentType());
assertNotNull(headers.getContentDisposition());
// assertEquals("attachment; filename=\"sample.pdf\"",
// headers.getContentDisposition().toString());
} catch (IOException e) {
fail("Exception thrown: " + e.getMessage());
}
}
@Test
public void testMultiPartFileToWebResponse() {
try {
byte[] fileContent = "Sample file content".getBytes();
MockMultipartFile file =
new MockMultipartFile("file", "sample.txt", "text/plain", fileContent);
ResponseEntity<byte[]> responseEntity =
WebResponseUtils.multiPartFileToWebResponse(file);
assertNotNull(responseEntity);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
assertNotNull(responseEntity.getBody());
HttpHeaders headers = responseEntity.getHeaders();
assertNotNull(headers);
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
assertNotNull(headers.getContentDisposition());
} catch (IOException e) {
fail("Exception thrown: " + e.getMessage());
}
}
@Test
public void testBytesToWebResponse() {
try {
byte[] bytes = "Sample bytes".getBytes();
String docName = "sample.txt";
MediaType mediaType = MediaType.TEXT_PLAIN;
ResponseEntity<byte[]> responseEntity =
WebResponseUtils.bytesToWebResponse(bytes, docName, mediaType);
assertNotNull(responseEntity);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
assertNotNull(responseEntity.getBody());
HttpHeaders headers = responseEntity.getHeaders();
assertNotNull(headers);
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
assertNotNull(headers.getContentDisposition());
} catch (IOException e) {
fail("Exception thrown: " + e.getMessage());
}
}
@Test
public void testPdfDocToWebResponse() {
try {
PDDocument document = new PDDocument();
document.addPage(new org.apache.pdfbox.pdmodel.PDPage());
String docName = "sample.pdf";
ResponseEntity<byte[]> responseEntity =
WebResponseUtils.pdfDocToWebResponse(document, docName);
assertNotNull(responseEntity);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
assertNotNull(responseEntity.getBody());
HttpHeaders headers = responseEntity.getHeaders();
assertNotNull(headers);
assertEquals(MediaType.APPLICATION_PDF, headers.getContentType());
assertNotNull(headers.getContentDisposition());
} catch (IOException e) {
fail("Exception thrown: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,108 @@
package stirling.software.common.util.misc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.IOException;
import java.lang.reflect.Method;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.api.misc.HighContrastColorCombination;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
class CustomColorReplaceStrategyTest {
private CustomColorReplaceStrategy strategy;
private MultipartFile mockFile;
@BeforeEach
void setUp() {
// Create a mock file
mockFile =
new MockMultipartFile(
"file", "test.pdf", "application/pdf", "test pdf content".getBytes());
// Initialize strategy with custom colors
strategy =
new CustomColorReplaceStrategy(
mockFile,
ReplaceAndInvert.CUSTOM_COLOR,
"000000", // Black text color
"FFFFFF", // White background color
null); // Not using high contrast combination for CUSTOM_COLOR
}
@Test
void testConstructor() {
// Test the constructor sets values correctly
assertNotNull(strategy, "Strategy should be initialized");
assertEquals(mockFile, strategy.getFileInput(), "File input should be set correctly");
assertEquals(
ReplaceAndInvert.CUSTOM_COLOR,
strategy.getReplaceAndInvert(),
"ReplaceAndInvert should be set correctly");
}
@Test
void testCheckSupportedFontForCharacter() throws Exception {
// Use reflection to access private method
Method method =
CustomColorReplaceStrategy.class.getDeclaredMethod(
"checkSupportedFontForCharacter", String.class);
method.setAccessible(true);
// Test with ASCII character which should be supported by standard fonts
Object result = method.invoke(strategy, "A");
assertNotNull(result, "Standard font should support ASCII character");
}
@Test
void testHighContrastColors() {
// Create a new strategy with HIGH_CONTRAST_COLOR setting
CustomColorReplaceStrategy highContrastStrategy =
new CustomColorReplaceStrategy(
mockFile,
ReplaceAndInvert.HIGH_CONTRAST_COLOR,
null, // These will be overridden by the high contrast settings
null,
HighContrastColorCombination.BLACK_TEXT_ON_WHITE);
// Verify the colors after replace() is called
try {
// Call replace (but we don't need the actual result for this test)
// This will throw IOException because we're using a mock file without actual PDF
// content
// but it will still set the colors according to the high contrast setting
try {
highContrastStrategy.replace();
} catch (IOException e) {
// Expected exception due to mock file
}
// Use reflection to access private fields
java.lang.reflect.Field textColorField =
CustomColorReplaceStrategy.class.getDeclaredField("textColor");
textColorField.setAccessible(true);
java.lang.reflect.Field backgroundColorField =
CustomColorReplaceStrategy.class.getDeclaredField("backgroundColor");
backgroundColorField.setAccessible(true);
String textColor = (String) textColorField.get(highContrastStrategy);
String backgroundColor = (String) backgroundColorField.get(highContrastStrategy);
// For BLACK_TEXT_ON_WHITE, text color should be "0" and background color should be
// "16777215"
assertEquals("0", textColor, "Text color should be black (0)");
assertEquals(
"16777215", backgroundColor, "Background color should be white (16777215)");
} catch (Exception e) {
// If we get here, the test failed
org.junit.jupiter.api.Assertions.fail("Exception occurred: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,109 @@
package stirling.software.common.util.misc;
import org.junit.jupiter.api.Test;
import stirling.software.common.model.api.misc.HighContrastColorCombination;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class HighContrastColorReplaceDeciderTest {
@Test
void testGetColors_BlackTextOnWhite() {
// Arrange
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
HighContrastColorCombination combination = HighContrastColorCombination.BLACK_TEXT_ON_WHITE;
// Act
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
// Assert
assertArrayEquals(
new String[] {"0", "16777215"},
colors,
"Should return black (0) for text and white (16777215) for background");
}
@Test
void testGetColors_GreenTextOnBlack() {
// Arrange
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
HighContrastColorCombination combination = HighContrastColorCombination.GREEN_TEXT_ON_BLACK;
// Act
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
// Assert
assertArrayEquals(
new String[] {"65280", "0"},
colors,
"Should return green (65280) for text and black (0) for background");
}
@Test
void testGetColors_WhiteTextOnBlack() {
// Arrange
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
HighContrastColorCombination combination = HighContrastColorCombination.WHITE_TEXT_ON_BLACK;
// Act
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
// Assert
assertArrayEquals(
new String[] {"16777215", "0"},
colors,
"Should return white (16777215) for text and black (0) for background");
}
@Test
void testGetColors_YellowTextOnBlack() {
// Arrange
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
HighContrastColorCombination combination =
HighContrastColorCombination.YELLOW_TEXT_ON_BLACK;
// Act
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
// Assert
assertArrayEquals(
new String[] {"16776960", "0"},
colors,
"Should return yellow (16776960) for text and black (0) for background");
}
@Test
void testGetColors_NullForInvalidCombination() {
// Arrange - use null for combination
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
// Act
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, null);
// Assert
assertNull(colors, "Should return null for invalid combination");
}
@Test
void testGetColors_ReplaceAndInvertParameterIsIgnored() {
// Arrange - use different ReplaceAndInvert values with the same combination
HighContrastColorCombination combination = HighContrastColorCombination.BLACK_TEXT_ON_WHITE;
// Act
String[] colors1 =
HighContrastColorReplaceDecider.getColors(
ReplaceAndInvert.HIGH_CONTRAST_COLOR, combination);
String[] colors2 =
HighContrastColorReplaceDecider.getColors(
ReplaceAndInvert.CUSTOM_COLOR, combination);
String[] colors3 =
HighContrastColorReplaceDecider.getColors(
ReplaceAndInvert.FULL_INVERSION, combination);
// Assert - all should return the same colors, showing that the ReplaceAndInvert parameter
// isn't used
assertArrayEquals(colors1, colors2, "ReplaceAndInvert parameter should be ignored");
assertArrayEquals(colors1, colors3, "ReplaceAndInvert parameter should be ignored");
}
}

View File

@@ -0,0 +1,152 @@
package stirling.software.common.util.misc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import javax.imageio.ImageIO;
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.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.InputStreamResource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
class InvertFullColorStrategyTest {
private InvertFullColorStrategy strategy;
private MultipartFile mockPdfFile;
@BeforeEach
void setUp() throws Exception {
// Create a simple PDF document for testing
byte[] pdfBytes = createSimplePdfWithRectangle();
mockPdfFile = new MockMultipartFile("file", "test.pdf", "application/pdf", pdfBytes);
// Create the strategy instance
strategy = new InvertFullColorStrategy(mockPdfFile, ReplaceAndInvert.FULL_INVERSION);
}
/** Helper method to create a simple PDF with a colored rectangle for testing */
private byte[] createSimplePdfWithRectangle() throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
// Add a filled rectangle with a specific color
PDPageContentStream contentStream = new PDPageContentStream(document, page);
contentStream.setNonStrokingColor(
new PDColor(new float[] {0.8f, 0.2f, 0.2f}, PDDeviceRGB.INSTANCE));
contentStream.addRect(100, 100, 400, 400);
contentStream.fill();
contentStream.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
return baos.toByteArray();
}
@Test
void testReplace() throws IOException {
// Test the replace method
InputStreamResource result = strategy.replace();
// Verify that the result is not null
assertNotNull(result, "The result should not be null");
}
@Test
void testInvertImageColors()
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// Create a test image with known colors
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics graphics = image.getGraphics();
graphics.setColor(new Color(200, 100, 50)); // RGB color to be inverted
graphics.fillRect(0, 0, 10, 10);
graphics.dispose();
// Get the color of a pixel before inversion
Color originalColor = new Color(image.getRGB(5, 5), true);
// Access private method using reflection
Method invertMethodRef =
InvertFullColorStrategy.class.getDeclaredMethod(
"invertImageColors", BufferedImage.class);
invertMethodRef.setAccessible(true);
// Invoke the private method
invertMethodRef.invoke(strategy, image);
// Get the color of the same pixel after inversion
Color invertedColor = new Color(image.getRGB(5, 5), true);
// Assert that the inversion worked correctly
assertEquals(
255 - originalColor.getRed(),
invertedColor.getRed(),
"Red channel should be inverted");
assertEquals(
255 - originalColor.getGreen(),
invertedColor.getGreen(),
"Green channel should be inverted");
assertEquals(
255 - originalColor.getBlue(),
invertedColor.getBlue(),
"Blue channel should be inverted");
}
@Test
void testConvertToBufferedImageTpFile()
throws NoSuchMethodException,
InvocationTargetException,
IllegalAccessException,
IOException {
// Create a test image
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
// Access private method using reflection
Method convertMethodRef =
InvertFullColorStrategy.class.getDeclaredMethod(
"convertToBufferedImageTpFile", BufferedImage.class);
convertMethodRef.setAccessible(true);
// Invoke the private method
File result = (File) convertMethodRef.invoke(strategy, image);
try {
// Assert that the file exists and is not empty
assertNotNull(result, "Result should not be null");
assertTrue(result.exists(), "File should exist");
assertTrue(result.length() > 0, "File should not be empty");
// Check that the file can be read back as an image
BufferedImage readBack = ImageIO.read(result);
assertNotNull(readBack, "Should be able to read back the image");
assertEquals(10, readBack.getWidth(), "Image width should match");
assertEquals(10, readBack.getHeight(), "Image height should match");
} finally {
// Clean up
if (result != null && result.exists()) {
Files.delete(result.toPath());
}
}
}
}

View File

@@ -0,0 +1,56 @@
package stirling.software.common.util.misc;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class PdfTextStripperCustomTest {
private PdfTextStripperCustom stripper;
private PDPage mockPage;
private PDRectangle mockMediaBox;
@BeforeEach
void setUp() throws IOException {
// Create the stripper instance
stripper = new PdfTextStripperCustom();
// Create mock objects
mockPage = mock(PDPage.class);
mockMediaBox = mock(PDRectangle.class);
// Configure mock behavior
when(mockPage.getMediaBox()).thenReturn(mockMediaBox);
when(mockMediaBox.getLowerLeftX()).thenReturn(0f);
when(mockMediaBox.getLowerLeftY()).thenReturn(0f);
when(mockMediaBox.getWidth()).thenReturn(612f);
when(mockMediaBox.getHeight()).thenReturn(792f);
}
@Test
void testConstructor() throws IOException {
// Verify that constructor doesn't throw an exception
PdfTextStripperCustom newStripper = new PdfTextStripperCustom();
assertNotNull(newStripper, "Constructor should create a non-null instance");
}
@Test
void testBasicFunctionality() throws IOException {
// Simply test that the method runs without exceptions
try {
stripper.addRegion("testRegion", new java.awt.geom.Rectangle2D.Float(0, 0, 100, 100));
stripper.extractRegions(mockPage);
assertTrue(true, "Should execute without errors");
} catch (Exception e) {
assertTrue(false, "Method should not throw exception: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,97 @@
package stirling.software.common.util.misc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.InputStreamResource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
class ReplaceAndInvertColorStrategyTest {
// A concrete implementation of the abstract class for testing
private static class ConcreteReplaceAndInvertColorStrategy
extends ReplaceAndInvertColorStrategy {
public ConcreteReplaceAndInvertColorStrategy(
MultipartFile file, ReplaceAndInvert replaceAndInvert) {
super(file, replaceAndInvert);
}
@Override
public InputStreamResource replace() throws IOException {
// Simple implementation for testing purposes
return new InputStreamResource(getFileInput().getInputStream());
}
}
@Test
void testConstructor() {
// Arrange
MultipartFile mockFile =
new MockMultipartFile(
"file", "test.pdf", "application/pdf", "test content".getBytes());
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
// Act
ReplaceAndInvertColorStrategy strategy =
new ConcreteReplaceAndInvertColorStrategy(mockFile, replaceAndInvert);
// Assert
assertNotNull(strategy, "Strategy should be initialized");
assertEquals(mockFile, strategy.getFileInput(), "File input should be set correctly");
assertEquals(
replaceAndInvert,
strategy.getReplaceAndInvert(),
"ReplaceAndInvert option should be set correctly");
}
@Test
void testReplace() throws IOException {
// Arrange
byte[] content = "test pdf content".getBytes();
MultipartFile mockFile =
new MockMultipartFile("file", "test.pdf", "application/pdf", content);
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
ReplaceAndInvertColorStrategy strategy =
new ConcreteReplaceAndInvertColorStrategy(mockFile, replaceAndInvert);
// Act
InputStreamResource result = strategy.replace();
// Assert
assertNotNull(result, "Result should not be null");
}
@Test
void testGettersAndSetters() {
// Arrange
MultipartFile mockFile1 =
new MockMultipartFile(
"file1", "test1.pdf", "application/pdf", "content1".getBytes());
MultipartFile mockFile2 =
new MockMultipartFile(
"file2", "test2.pdf", "application/pdf", "content2".getBytes());
// Act
ReplaceAndInvertColorStrategy strategy =
new ConcreteReplaceAndInvertColorStrategy(mockFile1, ReplaceAndInvert.CUSTOM_COLOR);
// Test initial values
assertEquals(mockFile1, strategy.getFileInput());
assertEquals(ReplaceAndInvert.CUSTOM_COLOR, strategy.getReplaceAndInvert());
// Test setters
strategy.setFileInput(mockFile2);
strategy.setReplaceAndInvert(ReplaceAndInvert.FULL_INVERSION);
// Assert new values
assertEquals(mockFile2, strategy.getFileInput());
assertEquals(ReplaceAndInvert.FULL_INVERSION, strategy.getReplaceAndInvert());
}
}

View File

@@ -0,0 +1,153 @@
package stirling.software.common.util.propertyeditor;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import stirling.software.common.model.api.security.RedactionArea;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class StringToArrayListPropertyEditorTest {
private StringToArrayListPropertyEditor editor;
@BeforeEach
void setUp() {
editor = new StringToArrayListPropertyEditor();
}
@Test
void testSetAsText_ValidJson() {
// Arrange
String json =
"[{\"x\":10.5,\"y\":20.5,\"width\":100.0,\"height\":50.0,\"page\":1,\"color\":\"#FF0000\"}]";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof List, "Value should be a List");
@SuppressWarnings("unchecked")
List<RedactionArea> list = (List<RedactionArea>) value;
assertEquals(1, list.size(), "List should have 1 entry");
RedactionArea area = list.get(0);
assertEquals(10.5, area.getX(), "X should be 10.5");
assertEquals(20.5, area.getY(), "Y should be 20.5");
assertEquals(100.0, area.getWidth(), "Width should be 100.0");
assertEquals(50.0, area.getHeight(), "Height should be 50.0");
assertEquals(1, area.getPage(), "Page should be 1");
assertEquals("#FF0000", area.getColor(), "Color should be #FF0000");
}
@Test
void testSetAsText_MultipleItems() {
// Arrange
String json =
"["
+ "{\"x\":10.0,\"y\":20.0,\"width\":100.0,\"height\":50.0,\"page\":1,\"color\":\"#FF0000\"},"
+ "{\"x\":30.0,\"y\":40.0,\"width\":200.0,\"height\":150.0,\"page\":2,\"color\":\"#00FF00\"}"
+ "]";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof List, "Value should be a List");
@SuppressWarnings("unchecked")
List<RedactionArea> list = (List<RedactionArea>) value;
assertEquals(2, list.size(), "List should have 2 entries");
RedactionArea area1 = list.get(0);
assertEquals(10.0, area1.getX(), "X should be 10.0");
assertEquals(20.0, area1.getY(), "Y should be 20.0");
assertEquals(1, area1.getPage(), "Page should be 1");
RedactionArea area2 = list.get(1);
assertEquals(30.0, area2.getX(), "X should be 30.0");
assertEquals(40.0, area2.getY(), "Y should be 40.0");
assertEquals(2, area2.getPage(), "Page should be 2");
}
@Test
void testSetAsText_EmptyString() {
// Arrange
String json = "";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof List, "Value should be a List");
@SuppressWarnings("unchecked")
List<RedactionArea> list = (List<RedactionArea>) value;
assertTrue(list.isEmpty(), "List should be empty");
}
@Test
void testSetAsText_NullString() {
// Act
editor.setAsText(null);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof List, "Value should be a List");
@SuppressWarnings("unchecked")
List<RedactionArea> list = (List<RedactionArea>) value;
assertTrue(list.isEmpty(), "List should be empty");
}
@Test
void testSetAsText_SingleItemAsArray() {
// Arrange - note this is a single object, not an array
String json =
"{\"x\":10.0,\"y\":20.0,\"width\":100.0,\"height\":50.0,\"page\":1,\"color\":\"#FF0000\"}";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof List, "Value should be a List");
@SuppressWarnings("unchecked")
List<RedactionArea> list = (List<RedactionArea>) value;
assertEquals(1, list.size(), "List should have 1 entry");
RedactionArea area = list.get(0);
assertEquals(10.0, area.getX(), "X should be 10.0");
assertEquals(20.0, area.getY(), "Y should be 20.0");
}
@Test
void testSetAsText_InvalidJson() {
// Arrange
String json = "invalid json";
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
}
@Test
void testSetAsText_InvalidStructure() {
// Arrange - this JSON doesn't match RedactionArea structure
String json = "[{\"invalid\":\"structure\"}]";
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
}
}

View File

@@ -0,0 +1,122 @@
package stirling.software.common.util.propertyeditor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class StringToMapPropertyEditorTest {
private StringToMapPropertyEditor editor;
@BeforeEach
void setUp() {
editor = new StringToMapPropertyEditor();
}
@Test
void testSetAsText_ValidJson() {
// Arrange
String json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof Map, "Value should be a Map");
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) value;
assertEquals(2, map.size(), "Map should have 2 entries");
assertEquals("value1", map.get("key1"), "First entry should be key1=value1");
assertEquals("value2", map.get("key2"), "Second entry should be key2=value2");
}
@Test
void testSetAsText_EmptyJson() {
// Arrange
String json = "{}";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof Map, "Value should be a Map");
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) value;
assertTrue(map.isEmpty(), "Map should be empty");
}
@Test
void testSetAsText_WhitespaceJson() {
// Arrange
String json = " { \"key1\" : \"value1\" } ";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof Map, "Value should be a Map");
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) value;
assertEquals(1, map.size(), "Map should have 1 entry");
assertEquals("value1", map.get("key1"), "Entry should be key1=value1");
}
@Test
void testSetAsText_NestedJson() {
// Arrange
String json = "{\"key1\":\"value1\",\"key2\":\"{\\\"nestedKey\\\":\\\"nestedValue\\\"}\"}";
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertTrue(value instanceof Map, "Value should be a Map");
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) value;
assertEquals(2, map.size(), "Map should have 2 entries");
assertEquals("value1", map.get("key1"), "First entry should be key1=value1");
assertEquals(
"{\"nestedKey\":\"nestedValue\"}",
map.get("key2"),
"Second entry should be the nested JSON as a string");
}
@Test
void testSetAsText_InvalidJson() {
// Arrange
String json = "{invalid json}";
// Act & Assert
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
assertEquals(
"Failed to convert java.lang.String to java.util.Map",
exception.getMessage(),
"Exception message should match expected error");
}
@Test
void testSetAsText_Null() {
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(null));
}
}