mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
fix(scan-library): prevent NPE during rescan for books with missing file associations (#2429)
Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
@@ -2,8 +2,8 @@ package com.adityachandel.booklore.service.library;
|
||||
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.model.dto.settings.LibraryFile;
|
||||
import com.adityachandel.booklore.model.entity.BookFileEntity;
|
||||
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||
import com.adityachandel.booklore.model.entity.BookFileEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryEntity;
|
||||
import com.adityachandel.booklore.model.websocket.LogNotification;
|
||||
import com.adityachandel.booklore.model.websocket.Topic;
|
||||
@@ -22,7 +22,8 @@ import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@AllArgsConstructor
|
||||
@@ -115,13 +116,19 @@ public class LibraryProcessingService {
|
||||
|
||||
return libraryEntity.getBookEntities().stream()
|
||||
.filter(book -> (book.getDeleted() == null || !book.getDeleted()))
|
||||
.filter(book -> !currentFullPaths.contains(book.getFullFilePath()))
|
||||
.filter(book -> {
|
||||
if (book.getBookFiles() == null || book.getBookFiles().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return !currentFullPaths.contains(book.getFullFilePath());
|
||||
})
|
||||
.map(BookEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
protected List<LibraryFile> detectNewBookPaths(List<LibraryFile> libraryFiles, LibraryEntity libraryEntity) {
|
||||
Set<String> existingKeys = libraryEntity.getBookEntities().stream()
|
||||
.filter(book -> book.getBookFiles() != null && !book.getBookFiles().isEmpty())
|
||||
.map(this::generateUniqueKey)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.adityachandel.booklore.service.library;
|
||||
|
||||
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
|
||||
import com.adityachandel.booklore.model.enums.LibraryScanMode;
|
||||
import com.adityachandel.booklore.repository.BookAdditionalFileRepository;
|
||||
import com.adityachandel.booklore.repository.LibraryRepository;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.task.options.RescanLibraryContext;
|
||||
import jakarta.persistence.EntityManager;
|
||||
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.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LibraryProcessingServiceRegressionTest {
|
||||
|
||||
@Mock
|
||||
private LibraryRepository libraryRepository;
|
||||
@Mock
|
||||
private NotificationService notificationService;
|
||||
@Mock
|
||||
private BookAdditionalFileRepository bookAdditionalFileRepository;
|
||||
@Mock
|
||||
private LibraryFileProcessorRegistry fileProcessorRegistry;
|
||||
@Mock
|
||||
private BookRestorationService bookRestorationService;
|
||||
@Mock
|
||||
private BookDeletionService bookDeletionService;
|
||||
@Mock
|
||||
private LibraryFileHelper libraryFileHelper;
|
||||
@Mock
|
||||
private EntityManager entityManager;
|
||||
@Mock
|
||||
private LibraryFileProcessor libraryFileProcessor;
|
||||
|
||||
private LibraryProcessingService libraryProcessingService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
libraryProcessingService = new LibraryProcessingService(
|
||||
libraryRepository,
|
||||
notificationService,
|
||||
bookAdditionalFileRepository,
|
||||
fileProcessorRegistry,
|
||||
bookRestorationService,
|
||||
bookDeletionService,
|
||||
libraryFileHelper,
|
||||
entityManager
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rescanLibrary_shouldThrowException_whenBookHasNoFiles(@TempDir Path tempDir) throws IOException {
|
||||
long libraryId = 1L;
|
||||
Path accessiblePath = tempDir.resolve("accessible");
|
||||
Files.createDirectory(accessiblePath);
|
||||
|
||||
LibraryEntity libraryEntity = new LibraryEntity();
|
||||
libraryEntity.setId(libraryId);
|
||||
libraryEntity.setName("Test Library");
|
||||
libraryEntity.setScanMode(LibraryScanMode.FILE_AS_BOOK);
|
||||
|
||||
LibraryPathEntity pathEntity = new LibraryPathEntity();
|
||||
pathEntity.setId(10L);
|
||||
pathEntity.setPath(accessiblePath.toString());
|
||||
libraryEntity.setLibraryPaths(List.of(pathEntity));
|
||||
|
||||
BookEntity bookWithNoFiles = new BookEntity();
|
||||
bookWithNoFiles.setId(1L);
|
||||
bookWithNoFiles.setLibraryPath(pathEntity);
|
||||
bookWithNoFiles.setBookFiles(Collections.emptyList()); // Empty files list
|
||||
|
||||
libraryEntity.setBookEntities(List.of(bookWithNoFiles));
|
||||
|
||||
when(libraryRepository.findById(libraryId)).thenReturn(Optional.of(libraryEntity));
|
||||
when(fileProcessorRegistry.getProcessor(libraryEntity)).thenReturn(libraryFileProcessor);
|
||||
// We need at least one file so it doesn't think the library is offline
|
||||
when(libraryFileHelper.getLibraryFiles(libraryEntity, libraryFileProcessor)).thenReturn(List.of(
|
||||
com.adityachandel.booklore.model.dto.settings.LibraryFile.builder()
|
||||
.libraryPathEntity(pathEntity)
|
||||
.fileName("other.epub")
|
||||
.fileSubPath("")
|
||||
.build()
|
||||
));
|
||||
|
||||
RescanLibraryContext context = RescanLibraryContext.builder().libraryId(libraryId).build();
|
||||
|
||||
// Should not throw exception anymore
|
||||
libraryProcessingService.rescanLibrary(context);
|
||||
|
||||
// Verify that the book with no files (ID 1) was detected as deleted
|
||||
verify(bookDeletionService).processDeletedLibraryFiles(
|
||||
argThat(list -> list.contains(1L)),
|
||||
any()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user