mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
Fix tests
This commit is contained in:
@@ -38,6 +38,7 @@ import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -148,9 +149,9 @@ public class BookMetadataService {
|
||||
.seriesName(request.getSeriesName())
|
||||
.seriesTotal(request.getSeriesTotal())
|
||||
.publishedDate(request.getPublishedDate())
|
||||
.categories(request.getGenres())
|
||||
.moods(request.getMoods())
|
||||
.tags(request.getTags())
|
||||
.categories(request.getGenres() != null ? request.getGenres() : Collections.emptySet())
|
||||
.moods(request.getMoods() != null ? request.getMoods() : Collections.emptySet())
|
||||
.tags(request.getTags() != null ? request.getTags() : Collections.emptySet())
|
||||
.build();
|
||||
|
||||
for (Long bookId : request.getBookIds()) {
|
||||
|
||||
@@ -52,7 +52,7 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
}
|
||||
|
||||
private static final List<IdentifierMapping> IDENTIFIER_PREFIX_MAPPINGS = List.of(
|
||||
new IdentifierMapping("urn:isbn:", "isbn", null), // Special handling for ISBN URNs
|
||||
new IdentifierMapping("urn:isbn:", "isbn", null),
|
||||
new IdentifierMapping("urn:amazon:", "asin", BookMetadata.BookMetadataBuilder::asin),
|
||||
new IdentifierMapping("urn:goodreads:", "goodreadsId", BookMetadata.BookMetadataBuilder::goodreadsId),
|
||||
new IdentifierMapping("urn:google:", "googleId", BookMetadata.BookMetadataBuilder::googleId),
|
||||
@@ -109,6 +109,17 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
if (coverImage == null) {
|
||||
String metaCoverId = findMetaCoverIdInOpf(epubFile);
|
||||
if (metaCoverId != null) {
|
||||
String href = findHrefForManifestId(epubFile, metaCoverId);
|
||||
if (href != null) {
|
||||
byte[] data = extractFileFromZip(epubFile, href);
|
||||
if (data != null) return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (coverImage == null) {
|
||||
for (io.documentnode.epub4j.domain.Resource res : epub.getResources().getAll()) {
|
||||
String id = res.getId();
|
||||
@@ -348,7 +359,6 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
if (value.startsWith(mapping.prefix)) {
|
||||
String extractedValue = value.substring(mapping.prefix.length());
|
||||
|
||||
// Special handling for ISBN URNs - pass to ISBN processor
|
||||
if ("isbn".equals(mapping.fieldName)) {
|
||||
processIsbnIdentifier(extractedValue, builder, processedFields);
|
||||
return true;
|
||||
@@ -472,7 +482,6 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
|
||||
value = value.trim();
|
||||
|
||||
// Check for year-only format first (e.g., "2024") - common in EPUB metadata
|
||||
if (YEAR_ONLY_PATTERN.matcher(value).matches()) {
|
||||
int year = Integer.parseInt(value);
|
||||
if (year >= 1 && year <= 9999) {
|
||||
@@ -490,7 +499,6 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Try parsing first 10 characters for ISO date format with extra content
|
||||
if (value.length() >= 10) {
|
||||
try {
|
||||
return LocalDate.parse(value.substring(0, 10));
|
||||
@@ -544,10 +552,91 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String findMetaCoverIdInOpf(File epubFile) {
|
||||
try (ZipFile zip = new ZipFile(epubFile)) {
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
dbf.setNamespaceAware(true);
|
||||
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
DocumentBuilder builder = dbf.newDocumentBuilder();
|
||||
|
||||
FileHeader containerHdr = zip.getFileHeader("META-INF/container.xml");
|
||||
if (containerHdr == null) return null;
|
||||
|
||||
try (InputStream cis = zip.getInputStream(containerHdr)) {
|
||||
Document containerDoc = builder.parse(cis);
|
||||
NodeList roots = containerDoc.getElementsByTagName("rootfile");
|
||||
if (roots.getLength() == 0) return null;
|
||||
|
||||
String opfPath = ((Element) roots.item(0)).getAttribute("full-path");
|
||||
if (StringUtils.isBlank(opfPath)) return null;
|
||||
|
||||
FileHeader opfHdr = zip.getFileHeader(opfPath);
|
||||
if (opfHdr == null) return null;
|
||||
|
||||
try (InputStream in = zip.getInputStream(opfHdr)) {
|
||||
Document doc = builder.parse(in);
|
||||
NodeList metaElements = doc.getElementsByTagName("meta");
|
||||
|
||||
for (int i = 0; i < metaElements.getLength(); i++) {
|
||||
Element meta = (Element) metaElements.item(i);
|
||||
String name = meta.getAttribute("name");
|
||||
if ("cover".equals(name)) {
|
||||
return meta.getAttribute("content");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to find meta cover ID in OPF: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String findHrefForManifestId(File epubFile, String manifestId) {
|
||||
try (ZipFile zip = new ZipFile(epubFile)) {
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
dbf.setNamespaceAware(true);
|
||||
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
DocumentBuilder builder = dbf.newDocumentBuilder();
|
||||
|
||||
FileHeader containerHdr = zip.getFileHeader("META-INF/container.xml");
|
||||
if (containerHdr == null) return null;
|
||||
|
||||
try (InputStream cis = zip.getInputStream(containerHdr)) {
|
||||
Document containerDoc = builder.parse(cis);
|
||||
NodeList roots = containerDoc.getElementsByTagName("rootfile");
|
||||
if (roots.getLength() == 0) return null;
|
||||
|
||||
String opfPath = ((Element) roots.item(0)).getAttribute("full-path");
|
||||
if (StringUtils.isBlank(opfPath)) return null;
|
||||
|
||||
FileHeader opfHdr = zip.getFileHeader(opfPath);
|
||||
if (opfHdr == null) return null;
|
||||
|
||||
try (InputStream in = zip.getInputStream(opfHdr)) {
|
||||
Document doc = builder.parse(in);
|
||||
NodeList manifestItems = doc.getElementsByTagName("item");
|
||||
|
||||
for (int i = 0; i < manifestItems.getLength(); i++) {
|
||||
Element item = (Element) manifestItems.item(i);
|
||||
String id = item.getAttribute("id");
|
||||
if (manifestId.equals(id)) {
|
||||
String href = item.getAttribute("href");
|
||||
String decodedHref = URLDecoder.decode(href, StandardCharsets.UTF_8);
|
||||
return resolvePath(opfPath, decodedHref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to find href for manifest ID {}: {}", manifestId, e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolvePath(String opfPath, String href) {
|
||||
if (href == null || href.isEmpty()) return null;
|
||||
|
||||
// If href is absolute within the zip (starts with /), return it without leading /
|
||||
if (href.startsWith("/")) return href.substring(1);
|
||||
|
||||
int lastSlash = opfPath.lastIndexOf('/');
|
||||
@@ -555,7 +644,6 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
|
||||
String combined = basePath + href;
|
||||
|
||||
// Normalize path components to handle ".." and "."
|
||||
java.util.LinkedList<String> parts = new java.util.LinkedList<>();
|
||||
for (String part : combined.split("/")) {
|
||||
if ("..".equals(part)) {
|
||||
@@ -635,4 +723,5 @@ public class EpubMetadataExtractor implements FileMetadataExtractor {
|
||||
log.debug("Failed to process Calibre user_metadata: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,11 @@ class BookMetadataServiceConcurrencyTest {
|
||||
private java.util.Optional<BookEntity> getUserBook(Long id) {
|
||||
BookEntity book = new BookEntity();
|
||||
book.setId(id);
|
||||
book.setMetadata(new BookMetadataEntity());
|
||||
|
||||
BookMetadataEntity metadata = new BookMetadataEntity();
|
||||
metadata.setCategories(java.util.Collections.emptySet());
|
||||
book.setMetadata(metadata);
|
||||
|
||||
return java.util.Optional.of(book);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +390,6 @@ class EpubMetadataExtractorTest {
|
||||
@Test
|
||||
@DisplayName("Should extract formatted ISBN-10 as ISBN-10, not ISBN-13")
|
||||
void extractMetadata_withFormattedIsbn10_returnsIsbn10() throws IOException {
|
||||
// "90-206-1280-8" is 13 characters long, causing it to be mistaken for ISBN-13 if formatting isn't stripped
|
||||
File epubFile = createEpubWithIsbn(null, "90-206-1280-8");
|
||||
|
||||
BookMetadata result = extractor.extractMetadata(epubFile);
|
||||
@@ -445,7 +444,6 @@ class EpubMetadataExtractorTest {
|
||||
@DisplayName("Should extract cover declared with properties='cover-image' even if ID/href doesn't contain 'cover'")
|
||||
void extractCover_propertiesCoverImage_returnsCoverBytes() throws IOException {
|
||||
byte[] pngImage = createMinimalPngImage();
|
||||
// Use an ID and HREF that do not contain "cover"
|
||||
File epubFile = createEpubWithPropertiesCover(pngImage, "image123", "images/img001.png");
|
||||
|
||||
byte[] cover = extractor.extractCover(epubFile);
|
||||
@@ -459,8 +457,6 @@ class EpubMetadataExtractorTest {
|
||||
@DisplayName("Should extract cover using meta name='cover' attribute fallback with URL-encoded href")
|
||||
void extractCover_metaCoverAttribute_returnsCoverBytes() throws IOException {
|
||||
byte[] pngImage = createMinimalPngImage();
|
||||
// Create EPUB where cover is only discoverable via <meta name="cover" content="img-id"/>
|
||||
// Use URL-encoded href (cover+.png -> cover%2B.png) to verify proper decoding
|
||||
File epubFile = createEpubWithMetaCoverAttribute(pngImage, "image-id", "images/img001%2B.png");
|
||||
|
||||
byte[] cover = extractor.extractCover(epubFile);
|
||||
@@ -782,55 +778,7 @@ class EpubMetadataExtractorTest {
|
||||
return epubFile;
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("URL Decoding Tests")
|
||||
class UrlDecodingTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should properly decode unicode characters in cover href")
|
||||
void extractCover_withUnicodeHref_decodesCorrectly() throws IOException {
|
||||
byte[] pngImage = createMinimalPngImage();
|
||||
String encodedHref = "cover%C3%A1.png"; // coverá.png URL-encoded
|
||||
File epubFile = createEpubWithUnicodeCover(pngImage, "cover_image", encodedHref);
|
||||
|
||||
byte[] cover = extractor.extractCover(epubFile);
|
||||
|
||||
assertNotNull(cover, "Cover should be extracted from EPUB with URL-encoded href");
|
||||
assertTrue(cover.length > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should extract cover with URL-encoded characters in manifest")
|
||||
void findCoverImageHrefInOpf_withEncodedHref_returnsDecodedPath() throws IOException {
|
||||
byte[] pngImage = createMinimalPngImage();
|
||||
String encodedHref = "images%2Fcover%C3%A1.jpg"; // images/coverá.jpg URL-encoded
|
||||
File epubFile = createEpubWithUnicodeCover(pngImage, "cover_image", encodedHref);
|
||||
|
||||
byte[] cover = extractor.extractCover(epubFile);
|
||||
|
||||
assertNotNull(cover, "Cover should be extracted even with encoded path");
|
||||
assertArrayEquals(pngImage, cover, "Extracted cover should match original image");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle multiple encoded unicode characters")
|
||||
void extractCover_withMultipleEncodedChars_handlesCorrectly() throws IOException {
|
||||
byte[] pngImage = createMinimalPngImage();
|
||||
String encodedHref = "c%C3%B3ver%20t%C3%ADtle%20%C3%A1nd%20%C3%B1ame.png";
|
||||
File epubFile = createEpubWithUnicodeCover(pngImage, "multi_unicode_cover", encodedHref);
|
||||
|
||||
byte[] cover = extractor.extractCover(epubFile);
|
||||
|
||||
assertNotNull(cover, "Cover should be extracted from filename with multiple encoded chars");
|
||||
assertTrue(cover.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
private File createEpubWithMetaCoverAttribute(byte[] coverImageData, String id, String encodedHref) throws IOException {
|
||||
// This EPUB uses <meta name="cover" content="id"/> to reference the cover image
|
||||
// The ID and href do NOT contain "cover" and there's no properties="cover-image"
|
||||
// This tests the fallback path: getMetaAttribute("cover") -> getById(coverId)
|
||||
// The href may be URL-encoded (e.g., image%2B.png for image+.png)
|
||||
String opfContent = String.format("""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0">
|
||||
@@ -855,9 +803,6 @@ class EpubMetadataExtractorTest {
|
||||
</container>
|
||||
""";
|
||||
|
||||
// Decode the href for the actual file path in the ZIP
|
||||
String decodedHref = java.net.URLDecoder.decode(encodedHref, StandardCharsets.UTF_8);
|
||||
|
||||
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(epubFile))) {
|
||||
zos.putNextEntry(new ZipEntry("mimetype"));
|
||||
zos.write("application/epub+zip".getBytes(StandardCharsets.UTF_8));
|
||||
@@ -871,7 +816,8 @@ class EpubMetadataExtractorTest {
|
||||
zos.write(opfContent.getBytes(StandardCharsets.UTF_8));
|
||||
zos.closeEntry();
|
||||
|
||||
zos.putNextEntry(new ZipEntry("OEBPS/" + decodedHref));
|
||||
String decodedPath = java.net.URLDecoder.decode(encodedHref, StandardCharsets.UTF_8);
|
||||
zos.putNextEntry(new ZipEntry("OEBPS/" + decodedPath));
|
||||
zos.write(coverImageData);
|
||||
zos.closeEntry();
|
||||
}
|
||||
@@ -916,7 +862,6 @@ class EpubMetadataExtractorTest {
|
||||
zos.write(opfContent.getBytes(StandardCharsets.UTF_8));
|
||||
zos.closeEntry();
|
||||
|
||||
// The actual file path in the zip should match the decoded href
|
||||
String decodedPath = java.net.URLDecoder.decode(encodedHref, java.nio.charset.StandardCharsets.UTF_8);
|
||||
zos.putNextEntry(new ZipEntry("OEBPS/" + decodedPath));
|
||||
zos.write(coverImageData);
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.adityachandel.booklore.model.enums.ReadStatus;
|
||||
import com.adityachandel.booklore.model.enums.ResetProgressType;
|
||||
import com.adityachandel.booklore.repository.*;
|
||||
import com.adityachandel.booklore.service.kobo.KoboReadingStateService;
|
||||
import com.adityachandel.booklore.service.hardcover.HardcoverSyncService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -42,6 +43,8 @@ class ReadingProgressServiceTest {
|
||||
private AuthenticationService authenticationService;
|
||||
@Mock
|
||||
private KoboReadingStateService koboReadingStateService;
|
||||
@Mock
|
||||
private HardcoverSyncService hardcoverSyncService;
|
||||
|
||||
@InjectMocks
|
||||
private ReadingProgressService readingProgressService;
|
||||
|
||||
Reference in New Issue
Block a user