changed response type, updated tests

This commit is contained in:
Dario Ghunney Ware 2025-06-18 15:13:27 +01:00
parent db8de7c087
commit 244cbe36ff
6 changed files with 165 additions and 45 deletions

View File

@ -1242,15 +1242,13 @@ public class EmlToPdf {
document, new ByteArrayInputStream(attachment.getData())); document, new ByteArrayInputStream(attachment.getData()));
embeddedFile.setSize(attachment.getData().length); embeddedFile.setSize(attachment.getData().length);
embeddedFile.setCreationDate(new GregorianCalendar()); embeddedFile.setCreationDate(new GregorianCalendar());
if (attachment.getContentType() != null) {
embeddedFile.setSubtype(attachment.getContentType());
}
// Create file specification // Create file specification
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification(); PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
fileSpec.setFile(uniqueFilename); fileSpec.setFile(uniqueFilename);
fileSpec.setEmbeddedFile(embeddedFile); fileSpec.setEmbeddedFile(embeddedFile);
if (attachment.getContentType() != null) { if (attachment.getContentType() != null) {
embeddedFile.setSubtype(attachment.getContentType());
fileSpec.setFileDescription("Email attachment: " + uniqueFilename); fileSpec.setFileDescription("Email attachment: " + uniqueFilename);
} }

View File

@ -60,10 +60,11 @@ public class AttachmentsController {
// Add attachments // Add attachments
catalog.setNames(documentNames); catalog.setNames(documentNames);
byte[] output =
pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments); pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.bytesToWebResponse(
document, output,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_with_attachments.pdf"); + "_with_attachments.pdf");

View File

@ -1,5 +1,6 @@
package stirling.software.SPDF.service; package stirling.software.SPDF.service;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.HashMap; import java.util.HashMap;
@ -13,6 +14,7 @@ import org.apache.pdfbox.pdmodel.PageMode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -25,7 +27,7 @@ import stirling.software.common.util.PDFAttachmentUtils;
public class PDFAttachmentService implements PDFAttachmentServiceInterface { public class PDFAttachmentService implements PDFAttachmentServiceInterface {
@Override @Override
public void addAttachment( public byte[] addAttachment(
PDDocument document, PDDocument document,
PDEmbeddedFilesNameTreeNode embeddedFilesTree, PDEmbeddedFilesNameTreeNode embeddedFilesTree,
List<MultipartFile> attachments) List<MultipartFile> attachments)
@ -34,6 +36,7 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface {
try { try {
existingNames = embeddedFilesTree.getNames(); existingNames = embeddedFilesTree.getNames();
if (existingNames == null) { if (existingNames == null) {
log.debug("No existing embedded files found, creating new names map."); log.debug("No existing embedded files found, creating new names map.");
existingNames = new HashMap<>(); existingNames = new HashMap<>();
@ -45,58 +48,54 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface {
throw e; throw e;
} }
grantAccessPermissions(document);
final Map<String, PDComplexFileSpecification> existingEmbeddedFiles = existingNames; final Map<String, PDComplexFileSpecification> existingEmbeddedFiles = existingNames;
attachments.forEach( attachments.forEach(
attachment -> { attachment -> {
// Create attachments specification String filename = attachment.getOriginalFilename();
PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification();
fileSpecification.setFile(attachment.getOriginalFilename());
fileSpecification.setFileUnicode(attachment.getOriginalFilename());
fileSpecification.setFileDescription(
"Embedded attachment: " + attachment.getOriginalFilename());
try { try {
// Create embedded attachment
PDEmbeddedFile embeddedFile = PDEmbeddedFile embeddedFile =
new PDEmbeddedFile(document, attachment.getInputStream()); new PDEmbeddedFile(document, attachment.getInputStream());
embeddedFile.setSize((int) attachment.getSize()); embeddedFile.setSize((int) attachment.getSize());
embeddedFile.setCreationDate(new GregorianCalendar()); embeddedFile.setCreationDate(new GregorianCalendar());
embeddedFile.setModDate(new GregorianCalendar()); embeddedFile.setModDate(new GregorianCalendar());
// Set MIME type if available
String contentType = attachment.getContentType(); String contentType = attachment.getContentType();
if (StringUtils.isNotBlank(contentType)) { if (StringUtils.isNotBlank(contentType)) {
embeddedFile.setSubtype(contentType); embeddedFile.setSubtype(contentType);
} }
// Associate embedded attachment with file specification // Create attachments specification and associate embedded attachment with
// file
PDComplexFileSpecification fileSpecification =
new PDComplexFileSpecification();
fileSpecification.setFile(filename);
fileSpecification.setFileUnicode(filename);
fileSpecification.setFileDescription("Embedded attachment: " + filename);
embeddedFile.setFile(fileSpecification); embeddedFile.setFile(fileSpecification);
fileSpecification.setEmbeddedFile(embeddedFile); fileSpecification.setEmbeddedFile(embeddedFile);
fileSpecification.setEmbeddedFileUnicode(embeddedFile); fileSpecification.setEmbeddedFileUnicode(embeddedFile);
// Add to the existing files map // Add to the existing files map
existingEmbeddedFiles.put( existingEmbeddedFiles.put(filename, fileSpecification);
attachment.getOriginalFilename(), fileSpecification);
log.info( log.info("Added attachment: {} ({} bytes)", filename, attachment.getSize());
"Added attachment: {} ({} bytes)",
attachment.getOriginalFilename(),
attachment.getSize());
} catch (IOException e) { } catch (IOException e) {
log.warn( log.warn("Failed to create embedded file for attachment: {}", filename, e);
"Failed to create embedded file for attachment: {}",
attachment.getOriginalFilename(),
e);
} }
}); });
embeddedFilesTree.setNames(existingNames); embeddedFilesTree.setNames(existingNames);
grantAccessPermissions(document);
PDFAttachmentUtils.setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS); PDFAttachmentUtils.setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
return output.toByteArray();
} }
private void grantAccessPermissions(PDDocument document) { private void grantAccessPermissions(PDDocument document) {
try {
AccessPermission currentPermissions = document.getCurrentAccessPermission(); AccessPermission currentPermissions = document.getCurrentAccessPermission();
currentPermissions.setCanAssembleDocument(true); currentPermissions.setCanAssembleDocument(true);
@ -109,5 +108,18 @@ public class PDFAttachmentService implements PDFAttachmentServiceInterface {
currentPermissions.setCanExtractContent(true); currentPermissions.setCanExtractContent(true);
currentPermissions.setCanExtractForAccessibility(true); currentPermissions.setCanExtractForAccessibility(true);
currentPermissions.setCanModifyAnnotations(true); currentPermissions.setCanModifyAnnotations(true);
var protectionPolicy = new StandardProtectionPolicy(null, null, currentPermissions);
if (!document.isAllSecurityToBeRemoved()) {
document.setAllSecurityToBeRemoved(true);
}
document.protect(protectionPolicy);
ByteArrayOutputStream output = new ByteArrayOutputStream();
document.save(output);
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
} }

View File

@ -9,7 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
public interface PDFAttachmentServiceInterface { public interface PDFAttachmentServiceInterface {
void addAttachment( byte[] addAttachment(
PDDocument document, PDDocument document,
PDEmbeddedFilesNameTreeNode efTree, PDEmbeddedFilesNameTreeNode efTree,
List<MultipartFile> attachments) List<MultipartFile> attachments)

View File

@ -61,16 +61,19 @@ class AttachmentsControllerTest {
@Test @Test
void addAttachments_WithExistingNames() throws IOException { void addAttachments_WithExistingNames() throws IOException {
List<MultipartFile> attachments = List.of(attachment1, attachment2); List<MultipartFile> attachments = List.of(attachment1, attachment2);
byte[] expectedOutput = "modified PDF content".getBytes();
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument); when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict); when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree); when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, attachments)).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments); ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments);
assertNotNull(response); assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfDocumentFactory).load(pdfFile, false); verify(pdfDocumentFactory).load(pdfFile, false);
verify(mockCatalog).setNames(mockNameDict); verify(mockCatalog).setNames(mockNameDict);
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments); verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments);
@ -79,14 +82,17 @@ class AttachmentsControllerTest {
@Test @Test
void addAttachments_WithoutExistingNames() throws IOException { void addAttachments_WithoutExistingNames() throws IOException {
List<MultipartFile> attachments = List.of(attachment1); List<MultipartFile> attachments = List.of(attachment1);
byte[] expectedOutput = "modified PDF content".getBytes();
try (PDDocument realDocument = new PDDocument()) { try (PDDocument realDocument = new PDDocument()) {
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(realDocument); when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(realDocument);
when(pdfAttachmentService.addAttachment(eq(realDocument), any(PDEmbeddedFilesNameTreeNode.class), eq(attachments))).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments); ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments);
assertNotNull(response); assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfDocumentFactory).load(pdfFile, false); verify(pdfDocumentFactory).load(pdfFile, false);
verify(pdfAttachmentService).addAttachment(eq(realDocument), any(PDEmbeddedFilesNameTreeNode.class), eq(attachments)); verify(pdfAttachmentService).addAttachment(eq(realDocument), any(PDEmbeddedFilesNameTreeNode.class), eq(attachments));
} }
@ -113,7 +119,7 @@ class AttachmentsControllerTest {
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict); when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree); when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
doThrow(ioException).when(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments); when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, attachments)).thenThrow(ioException);
assertThrows(IOException.class, () -> attachmentsController.addAttachments(pdfFile, attachments)); assertThrows(IOException.class, () -> attachmentsController.addAttachments(pdfFile, attachments));
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments); verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments);
@ -162,16 +168,19 @@ class AttachmentsControllerTest {
@Test @Test
void addAttachments_EmptyAttachmentsList() throws IOException { void addAttachments_EmptyAttachmentsList() throws IOException {
List<MultipartFile> emptyAttachments = List.of(); List<MultipartFile> emptyAttachments = List.of();
byte[] expectedOutput = "PDF content without new attachments".getBytes();
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument); when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict); when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree); when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, emptyAttachments)).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, emptyAttachments); ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, emptyAttachments);
assertNotNull(response); assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, emptyAttachments); verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, emptyAttachments);
} }
@ -179,16 +188,19 @@ class AttachmentsControllerTest {
void addAttachments_NullFilename() throws IOException { void addAttachments_NullFilename() throws IOException {
MockMultipartFile attachmentWithNullName = new MockMultipartFile("attachment", null, "text/plain", "content".getBytes()); MockMultipartFile attachmentWithNullName = new MockMultipartFile("attachment", null, "text/plain", "content".getBytes());
List<MultipartFile> attachments = List.of(attachmentWithNullName); List<MultipartFile> attachments = List.of(attachmentWithNullName);
byte[] expectedOutput = "PDF with null filename attachment".getBytes();
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument); when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog); when(mockDocument.getDocumentCatalog()).thenReturn(mockCatalog);
when(mockCatalog.getNames()).thenReturn(mockNameDict); when(mockCatalog.getNames()).thenReturn(mockNameDict);
when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree); when(mockNameDict.getEmbeddedFiles()).thenReturn(mockEmbeddedFilesTree);
when(pdfAttachmentService.addAttachment(mockDocument, mockEmbeddedFilesTree, attachments)).thenReturn(expectedOutput);
ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments); ResponseEntity<byte[]> response = attachmentsController.addAttachments(pdfFile, attachments);
assertNotNull(response); assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments); verify(pdfAttachmentService).addAttachment(mockDocument, mockEmbeddedFilesTree, attachments);
} }

View File

@ -7,10 +7,14 @@ import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -39,8 +43,10 @@ class PDFAttachmentServiceTest {
when(attachments.get(0).getSize()).thenReturn(12L); when(attachments.get(0).getSize()).thenReturn(12L);
when(attachments.get(0).getContentType()).thenReturn("text/plain"); when(attachments.get(0).getContentType()).thenReturn("text/plain");
pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments); byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap()); verify(embeddedFilesTree).setNames(anyMap());
} }
} }
@ -58,8 +64,10 @@ class PDFAttachmentServiceTest {
when(attachments.get(0).getSize()).thenReturn(15L); when(attachments.get(0).getSize()).thenReturn(15L);
when(attachments.get(0).getContentType()).thenReturn("application/pdf"); when(attachments.get(0).getContentType()).thenReturn("application/pdf");
pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments); byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap()); verify(embeddedFilesTree).setNames(anyMap());
} }
} }
@ -78,8 +86,10 @@ class PDFAttachmentServiceTest {
when(attachments.get(0).getSize()).thenReturn(25L); when(attachments.get(0).getSize()).thenReturn(25L);
when(attachments.get(0).getContentType()).thenReturn(""); when(attachments.get(0).getContentType()).thenReturn("");
pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments); byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap()); verify(embeddedFilesTree).setNames(anyMap());
} }
} }
@ -111,8 +121,95 @@ class PDFAttachmentServiceTest {
when(attachments.get(0).getInputStream()).thenThrow(ioException); when(attachments.get(0).getInputStream()).thenThrow(ioException);
when(attachments.get(0).getSize()).thenReturn(10L); when(attachments.get(0).getSize()).thenReturn(10L);
pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments); byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithProtectedDocument() throws IOException {
try (var document = new PDDocument()) {
// Create a document with restricted permissions (this simulates an encrypted/protected document)
AccessPermission ap = new AccessPermission();
ap.setCanExtractContent(false); // Restrict content extraction initially
var spp = new StandardProtectionPolicy("owner", "user", ap);
document.protect(spp);
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
when(attachments.get(0).getOriginalFilename()).thenReturn("test.txt");
when(attachments.get(0).getInputStream()).thenReturn(
new ByteArrayInputStream("Test content".getBytes()));
when(attachments.get(0).getSize()).thenReturn(12L);
when(attachments.get(0).getContentType()).thenReturn("text/plain");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithRestrictedPermissions() throws IOException {
try (var document = new PDDocument()) {
// Create a document with very restricted permissions that should block permission changes
AccessPermission ap = new AccessPermission();
ap.setCanModify(false);
ap.setCanAssembleDocument(false);
ap.setCanExtractContent(false);
var spp = new StandardProtectionPolicy("owner", "user", ap);
document.protect(spp);
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
when(attachments.get(0).getOriginalFilename()).thenReturn("test.txt");
when(attachments.get(0).getInputStream()).thenReturn(
new ByteArrayInputStream("Test content".getBytes()));
when(attachments.get(0).getSize()).thenReturn(12L);
when(attachments.get(0).getContentType()).thenReturn("text/plain");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
verify(embeddedFilesTree).setNames(anyMap());
}
}
@Test
void addAttachmentToPDF_WithNonEncryptedDocument() throws IOException {
try (var document = new PDDocument()) {
var embeddedFilesTree = mock(PDEmbeddedFilesNameTreeNode.class);
var attachments = List.of(mock(MultipartFile.class));
var existingNames = new HashMap<String, PDComplexFileSpecification>();
when(embeddedFilesTree.getNames()).thenReturn(existingNames);
when(attachments.get(0).getOriginalFilename()).thenReturn("test.txt");
when(attachments.get(0).getInputStream()).thenReturn(
new ByteArrayInputStream("Test content".getBytes()));
when(attachments.get(0).getSize()).thenReturn(12L);
when(attachments.get(0).getContentType()).thenReturn("text/plain");
byte[] result = pdfAttachmentService.addAttachment(document, embeddedFilesTree, attachments);
assertNotNull(result);
assertTrue(result.length > 0);
// Verify permissions are set correctly for non-encrypted documents
AccessPermission permissions = document.getCurrentAccessPermission();
assertTrue(permissions.canExtractContent());
assertTrue(permissions.canExtractForAccessibility());
assertTrue(permissions.canModifyAnnotations());
verify(embeddedFilesTree).setNames(anyMap()); verify(embeddedFilesTree).setNames(anyMap());
} }
} }