mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Automatically add newly added books to Kobo shelf (#1826)
This commit is contained in:
@@ -3,7 +3,6 @@ package com.adityachandel.booklore.controller;
|
|||||||
import com.adityachandel.booklore.model.dto.KoboSyncSettings;
|
import com.adityachandel.booklore.model.dto.KoboSyncSettings;
|
||||||
import com.adityachandel.booklore.service.kobo.KoboSettingsService;
|
import com.adityachandel.booklore.service.kobo.KoboSettingsService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -31,31 +30,19 @@ public class KoboSettingsController {
|
|||||||
|
|
||||||
@Operation(summary = "Create or update Kobo token", description = "Create or update the Kobo sync token for the current user. Requires sync permission or admin.")
|
@Operation(summary = "Create or update Kobo token", description = "Create or update the Kobo sync token for the current user. Requires sync permission or admin.")
|
||||||
@ApiResponse(responseCode = "200", description = "Token created/updated successfully")
|
@ApiResponse(responseCode = "200", description = "Token created/updated successfully")
|
||||||
@PutMapping
|
@PutMapping("/token")
|
||||||
@PreAuthorize("@securityUtil.canSyncKobo() or @securityUtil.isAdmin()")
|
@PreAuthorize("@securityUtil.canSyncKobo() or @securityUtil.isAdmin()")
|
||||||
public ResponseEntity<KoboSyncSettings> createOrUpdateToken() {
|
public ResponseEntity<KoboSyncSettings> createOrUpdateToken() {
|
||||||
KoboSyncSettings updated = koboService.createOrUpdateToken();
|
KoboSyncSettings updated = koboService.createOrUpdateToken();
|
||||||
return ResponseEntity.ok(updated);
|
return ResponseEntity.ok(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "Toggle Kobo sync", description = "Enable or disable Kobo sync for the current user. Requires sync permission or admin.")
|
@Operation(summary = "Update Kobo settings", description = "Update Kobo sync settings for the current user. Requires sync permission or admin.")
|
||||||
@ApiResponse(responseCode = "204", description = "Sync toggled successfully")
|
@ApiResponse(responseCode = "200", description = "Settings updated successfully")
|
||||||
@PutMapping("/sync")
|
@PutMapping
|
||||||
@PreAuthorize("@securityUtil.canSyncKobo() or @securityUtil.isAdmin()")
|
@PreAuthorize("@securityUtil.canSyncKobo() or @securityUtil.isAdmin()")
|
||||||
public ResponseEntity<Void> toggleSync(
|
public ResponseEntity<KoboSyncSettings> updateSettings(@RequestBody KoboSyncSettings settings) {
|
||||||
@Parameter(description = "Enable or disable sync") @RequestParam boolean enabled) {
|
KoboSyncSettings updated = koboService.updateSettings(settings);
|
||||||
koboService.setSyncEnabled(enabled);
|
|
||||||
return ResponseEntity.noContent().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Update progress thresholds", description = "Update the progress thresholds for marking books as reading or finished. Requires sync permission or admin.")
|
|
||||||
@ApiResponse(responseCode = "200", description = "Thresholds updated successfully")
|
|
||||||
@PutMapping("/progress-thresholds")
|
|
||||||
@PreAuthorize("@securityUtil.canSyncKobo() or @securityUtil.isAdmin()")
|
|
||||||
public ResponseEntity<KoboSyncSettings> updateProgressThresholds(
|
|
||||||
@Parameter(description = "Progress percentage to mark as reading (0-100)") @RequestParam(required = false) Float readingThreshold,
|
|
||||||
@Parameter(description = "Progress percentage to mark as finished (0-100)") @RequestParam(required = false) Float finishedThreshold) {
|
|
||||||
KoboSyncSettings updated = koboService.updateProgressThresholds(readingThreshold, finishedThreshold);
|
|
||||||
return ResponseEntity.ok(updated);
|
return ResponseEntity.ok(updated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ public class KoboSyncSettings {
|
|||||||
private boolean syncEnabled;
|
private boolean syncEnabled;
|
||||||
private Float progressMarkAsReadingThreshold;
|
private Float progressMarkAsReadingThreshold;
|
||||||
private Float progressMarkAsFinishedThreshold;
|
private Float progressMarkAsFinishedThreshold;
|
||||||
|
private boolean autoAddToShelf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,8 @@ public class KoboUserSettingsEntity {
|
|||||||
@Column(name = "progress_mark_as_finished_threshold")
|
@Column(name = "progress_mark_as_finished_threshold")
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Float progressMarkAsFinishedThreshold = 99f;
|
private Float progressMarkAsFinishedThreshold = 99f;
|
||||||
|
|
||||||
|
@Column(name = "auto_add_to_shelf")
|
||||||
|
@Builder.Default
|
||||||
|
private boolean autoAddToShelf = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.adityachandel.booklore.model.entity.KoboUserSettingsEntity;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@@ -12,4 +13,6 @@ public interface KoboUserSettingsRepository extends JpaRepository<KoboUserSettin
|
|||||||
Optional<KoboUserSettingsEntity> findByUserId(Long userId);
|
Optional<KoboUserSettingsEntity> findByUserId(Long userId);
|
||||||
|
|
||||||
Optional<KoboUserSettingsEntity> findByToken(String token);
|
Optional<KoboUserSettingsEntity> findByToken(String token);
|
||||||
|
|
||||||
|
List<KoboUserSettingsEntity> findByAutoAddToShelfTrueAndSyncEnabledTrue();
|
||||||
}
|
}
|
||||||
@@ -18,4 +18,6 @@ public interface ShelfRepository extends JpaRepository<ShelfEntity, Long> {
|
|||||||
List<ShelfEntity> findByUserId(Long id);
|
List<ShelfEntity> findByUserId(Long id);
|
||||||
|
|
||||||
Optional<ShelfEntity> findByUserIdAndName(Long id, String name);
|
Optional<ShelfEntity> findByUserIdAndName(Long id, String name);
|
||||||
|
|
||||||
|
List<ShelfEntity> findByUserIdInAndName(List<Long> userIds, String name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.adityachandel.booklore.service.kobo;
|
||||||
|
|
||||||
|
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||||
|
import com.adityachandel.booklore.model.entity.KoboUserSettingsEntity;
|
||||||
|
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
||||||
|
import com.adityachandel.booklore.model.enums.ShelfType;
|
||||||
|
import com.adityachandel.booklore.repository.BookRepository;
|
||||||
|
import com.adityachandel.booklore.repository.KoboUserSettingsRepository;
|
||||||
|
import com.adityachandel.booklore.repository.ShelfRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class KoboAutoShelfService {
|
||||||
|
|
||||||
|
private final KoboUserSettingsRepository koboUserSettingsRepository;
|
||||||
|
private final ShelfRepository shelfRepository;
|
||||||
|
private final BookRepository bookRepository;
|
||||||
|
private final KoboCompatibilityService koboCompatibilityService;
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void autoAddBookToKoboShelves(Long bookId) {
|
||||||
|
if (bookId == null) {
|
||||||
|
log.warn("Book ID is null for auto-add to Kobo shelf");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookEntity book = bookRepository.findById(bookId).orElse(null);
|
||||||
|
if (!isBookEligible(book)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KoboUserSettingsEntity> eligibleUsers = koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue();
|
||||||
|
|
||||||
|
if (eligibleUsers.isEmpty()) {
|
||||||
|
log.debug("No Kobo auto-add enabled users for book {}", book.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> userIds = eligibleUsers.stream()
|
||||||
|
.map(KoboUserSettingsEntity::getUserId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<ShelfEntity> shelves = shelfRepository.findByUserIdInAndName(userIds, ShelfType.KOBO.getName());
|
||||||
|
|
||||||
|
Map<Long, ShelfEntity> shelfByUser = shelves
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(s -> s.getUser().getId(), s -> s));
|
||||||
|
|
||||||
|
boolean modified = false;
|
||||||
|
|
||||||
|
for (KoboUserSettingsEntity setting : eligibleUsers) {
|
||||||
|
ShelfEntity shelf = shelfByUser.get(setting.getUserId());
|
||||||
|
|
||||||
|
if (shelf == null) {
|
||||||
|
log.debug("User {} has auto-add enabled but no Kobo shelf exists", setting.getUserId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (book.getShelves().contains(shelf)) {
|
||||||
|
log.debug("Book {} already on Kobo shelf for user {}", book.getId(), setting.getUserId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
book.getShelves().add(shelf);
|
||||||
|
modified = true;
|
||||||
|
log.info("Auto-added book {} to Kobo shelf for user {}", book.getId(), setting.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
bookRepository.save(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBookEligible(BookEntity book) {
|
||||||
|
if (book == null) {
|
||||||
|
log.warn("Book not found for Kobo auto-add");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!koboCompatibilityService.isBookSupportedForKobo(book)) {
|
||||||
|
log.debug("Book {} is not Kobo-compatible, skipping", book.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.adityachandel.booklore.model.dto.Shelf;
|
|||||||
import com.adityachandel.booklore.model.dto.request.ShelfCreateRequest;
|
import com.adityachandel.booklore.model.dto.request.ShelfCreateRequest;
|
||||||
import com.adityachandel.booklore.model.entity.KoboUserSettingsEntity;
|
import com.adityachandel.booklore.model.entity.KoboUserSettingsEntity;
|
||||||
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
||||||
|
import com.adityachandel.booklore.model.enums.IconType;
|
||||||
import com.adityachandel.booklore.model.enums.ShelfType;
|
import com.adityachandel.booklore.model.enums.ShelfType;
|
||||||
import com.adityachandel.booklore.repository.KoboUserSettingsRepository;
|
import com.adityachandel.booklore.repository.KoboUserSettingsRepository;
|
||||||
import com.adityachandel.booklore.service.ShelfService;
|
import com.adityachandel.booklore.service.ShelfService;
|
||||||
@@ -56,34 +57,31 @@ public class KoboSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void setSyncEnabled(boolean enabled) {
|
public KoboSyncSettings updateSettings(KoboSyncSettings settings) {
|
||||||
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
||||||
KoboUserSettingsEntity entity = repository.findByUserId(user.getId()).orElseThrow(() -> new IllegalStateException("Kobo settings not found for user"));
|
KoboUserSettingsEntity entity = repository.findByUserId(user.getId()).orElseGet(() -> initDefaultSettings(user.getId()));
|
||||||
Shelf userKoboShelf = shelfService.getUserKoboShelf();
|
|
||||||
if (!enabled) {
|
|
||||||
if (userKoboShelf != null) {
|
|
||||||
shelfService.deleteShelf(userKoboShelf.getId());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ensureKoboShelfExists(user.getId());
|
|
||||||
}
|
|
||||||
entity.setSyncEnabled(enabled);
|
|
||||||
repository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
if (settings.isSyncEnabled() != entity.isSyncEnabled()) {
|
||||||
public KoboSyncSettings updateProgressThresholds(Float readingThreshold, Float finishedThreshold) {
|
Shelf userKoboShelf = shelfService.getUserKoboShelf();
|
||||||
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
if (!settings.isSyncEnabled()) {
|
||||||
KoboUserSettingsEntity entity = repository.findByUserId(user.getId())
|
if (userKoboShelf != null) {
|
||||||
.orElseGet(() -> initDefaultSettings(user.getId()));
|
shelfService.deleteShelf(userKoboShelf.getId());
|
||||||
|
}
|
||||||
if (readingThreshold != null) {
|
} else {
|
||||||
entity.setProgressMarkAsReadingThreshold(readingThreshold);
|
ensureKoboShelfExists(user.getId());
|
||||||
|
}
|
||||||
|
entity.setSyncEnabled(settings.isSyncEnabled());
|
||||||
}
|
}
|
||||||
if (finishedThreshold != null) {
|
|
||||||
entity.setProgressMarkAsFinishedThreshold(finishedThreshold);
|
if (settings.getProgressMarkAsReadingThreshold() != null) {
|
||||||
|
entity.setProgressMarkAsReadingThreshold(settings.getProgressMarkAsReadingThreshold());
|
||||||
}
|
}
|
||||||
|
if (settings.getProgressMarkAsFinishedThreshold() != null) {
|
||||||
|
entity.setProgressMarkAsFinishedThreshold(settings.getProgressMarkAsFinishedThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.setAutoAddToShelf(settings.isAutoAddToShelf());
|
||||||
|
|
||||||
repository.save(entity);
|
repository.save(entity);
|
||||||
return mapToDto(entity);
|
return mapToDto(entity);
|
||||||
}
|
}
|
||||||
@@ -105,6 +103,7 @@ public class KoboSettingsService {
|
|||||||
ShelfCreateRequest.builder()
|
ShelfCreateRequest.builder()
|
||||||
.name(ShelfType.KOBO.getName())
|
.name(ShelfType.KOBO.getName())
|
||||||
.icon(ShelfType.KOBO.getIcon())
|
.icon(ShelfType.KOBO.getIcon())
|
||||||
|
.iconType(IconType.PRIME_NG)
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,6 +121,7 @@ public class KoboSettingsService {
|
|||||||
dto.setSyncEnabled(entity.isSyncEnabled());
|
dto.setSyncEnabled(entity.isSyncEnabled());
|
||||||
dto.setProgressMarkAsReadingThreshold(entity.getProgressMarkAsReadingThreshold());
|
dto.setProgressMarkAsReadingThreshold(entity.getProgressMarkAsReadingThreshold());
|
||||||
dto.setProgressMarkAsFinishedThreshold(entity.getProgressMarkAsFinishedThreshold());
|
dto.setProgressMarkAsFinishedThreshold(entity.getProgressMarkAsFinishedThreshold());
|
||||||
|
dto.setAutoAddToShelf(entity.isAutoAddToShelf());
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.adityachandel.booklore.model.enums.LibraryScanMode;
|
|||||||
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
||||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||||
|
import com.adityachandel.booklore.service.kobo.KoboAutoShelfService;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -22,6 +23,7 @@ public class FileAsBookProcessor implements LibraryFileProcessor {
|
|||||||
|
|
||||||
private final BookEventBroadcaster bookEventBroadcaster;
|
private final BookEventBroadcaster bookEventBroadcaster;
|
||||||
private final BookFileProcessorRegistry processorRegistry;
|
private final BookFileProcessorRegistry processorRegistry;
|
||||||
|
private final KoboAutoShelfService koboAutoShelfService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LibraryScanMode getScanMode() {
|
public LibraryScanMode getScanMode() {
|
||||||
@@ -43,6 +45,7 @@ public class FileAsBookProcessor implements LibraryFileProcessor {
|
|||||||
FileProcessResult result = processLibraryFile(libraryFile);
|
FileProcessResult result = processLibraryFile(libraryFile);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
bookEventBroadcaster.broadcastBookAddEvent(result.getBook());
|
bookEventBroadcaster.broadcastBookAddEvent(result.getBook());
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(result.getBook().getId());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to process file '{}': {}", libraryFile.getFileName(), e.getMessage());
|
log.error("Failed to process file '{}': {}", libraryFile.getFileName(), e.getMessage());
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kobo_user_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS auto_add_to_shelf BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package com.adityachandel.booklore.service.kobo;
|
||||||
|
|
||||||
|
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||||
|
import com.adityachandel.booklore.model.entity.BookLoreUserEntity;
|
||||||
|
import com.adityachandel.booklore.model.entity.KoboUserSettingsEntity;
|
||||||
|
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
||||||
|
import com.adityachandel.booklore.model.enums.ShelfType;
|
||||||
|
import com.adityachandel.booklore.repository.BookRepository;
|
||||||
|
import com.adityachandel.booklore.repository.KoboUserSettingsRepository;
|
||||||
|
import com.adityachandel.booklore.repository.ShelfRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class KoboAutoShelfServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KoboUserSettingsRepository koboUserSettingsRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ShelfRepository shelfRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BookRepository bookRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KoboCompatibilityService koboCompatibilityService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private KoboAutoShelfService koboAutoShelfService;
|
||||||
|
|
||||||
|
private BookEntity testBook;
|
||||||
|
private BookLoreUserEntity testUser1;
|
||||||
|
private BookLoreUserEntity testUser2;
|
||||||
|
private ShelfEntity koboShelf1;
|
||||||
|
private ShelfEntity koboShelf2;
|
||||||
|
private KoboUserSettingsEntity settings1;
|
||||||
|
private KoboUserSettingsEntity settings2;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
testBook = BookEntity.builder()
|
||||||
|
.id(1L)
|
||||||
|
.shelves(new HashSet<>())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
testUser1 = BookLoreUserEntity.builder()
|
||||||
|
.id(100L)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
testUser2 = BookLoreUserEntity.builder()
|
||||||
|
.id(200L)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
koboShelf1 = ShelfEntity.builder()
|
||||||
|
.id(10L)
|
||||||
|
.name(ShelfType.KOBO.getName())
|
||||||
|
.user(testUser1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
koboShelf2 = ShelfEntity.builder()
|
||||||
|
.id(20L)
|
||||||
|
.name(ShelfType.KOBO.getName())
|
||||||
|
.user(testUser2)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
settings1 = KoboUserSettingsEntity.builder()
|
||||||
|
.userId(100L)
|
||||||
|
.autoAddToShelf(true)
|
||||||
|
.syncEnabled(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
settings2 = KoboUserSettingsEntity.builder()
|
||||||
|
.userId(200L)
|
||||||
|
.autoAddToShelf(true)
|
||||||
|
.syncEnabled(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withNullBookId_shouldReturnEarly() {
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(null);
|
||||||
|
|
||||||
|
verify(bookRepository, never()).findById(anyLong());
|
||||||
|
verify(koboUserSettingsRepository, never()).findByAutoAddToShelfTrueAndSyncEnabledTrue();
|
||||||
|
verify(shelfRepository, never()).findByUserIdInAndName(anyList(), anyString());
|
||||||
|
verify(bookRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withNonExistentBook_shouldReturnEarly() {
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository).findById(1L);
|
||||||
|
verify(koboUserSettingsRepository, never()).findByAutoAddToShelfTrueAndSyncEnabledTrue();
|
||||||
|
verify(shelfRepository, never()).findByUserIdInAndName(anyList(), anyString());
|
||||||
|
verify(bookRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withIncompatibleBook_shouldReturnEarly() {
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(false);
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository).findById(1L);
|
||||||
|
verify(koboCompatibilityService).isBookSupportedForKobo(testBook);
|
||||||
|
verify(koboUserSettingsRepository, never()).findByAutoAddToShelfTrueAndSyncEnabledTrue();
|
||||||
|
verify(shelfRepository, never()).findByUserIdInAndName(anyList(), anyString());
|
||||||
|
verify(bookRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withNoEligibleUsers_shouldReturnEarly() {
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository).findById(1L);
|
||||||
|
verify(koboCompatibilityService).isBookSupportedForKobo(testBook);
|
||||||
|
verify(koboUserSettingsRepository).findByAutoAddToShelfTrueAndSyncEnabledTrue();
|
||||||
|
verify(shelfRepository, never()).findByUserIdInAndName(anyList(), anyString());
|
||||||
|
verify(bookRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_successfully_shouldAddBookToShelves() {
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(List.of(settings1, settings2));
|
||||||
|
when(shelfRepository.findByUserIdInAndName(List.of(100L, 200L), ShelfType.KOBO.getName()))
|
||||||
|
.thenReturn(List.of(koboShelf1, koboShelf2));
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository).findById(1L);
|
||||||
|
verify(koboCompatibilityService).isBookSupportedForKobo(testBook);
|
||||||
|
verify(koboUserSettingsRepository).findByAutoAddToShelfTrueAndSyncEnabledTrue();
|
||||||
|
verify(shelfRepository).findByUserIdInAndName(List.of(100L, 200L), ShelfType.KOBO.getName());
|
||||||
|
verify(bookRepository).save(testBook);
|
||||||
|
|
||||||
|
assert testBook.getShelves().contains(koboShelf1);
|
||||||
|
assert testBook.getShelves().contains(koboShelf2);
|
||||||
|
assert testBook.getShelves().size() == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withOneUserMissingShelf_shouldAddOnlyToExistingShelves() {
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(List.of(settings1, settings2));
|
||||||
|
when(shelfRepository.findByUserIdInAndName(List.of(100L, 200L), ShelfType.KOBO.getName()))
|
||||||
|
.thenReturn(List.of(koboShelf1));
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository).save(testBook);
|
||||||
|
assert testBook.getShelves().contains(koboShelf1);
|
||||||
|
assert !testBook.getShelves().contains(koboShelf2);
|
||||||
|
assert testBook.getShelves().size() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withBookAlreadyOnShelf_shouldNotDuplicate() {
|
||||||
|
testBook.getShelves().add(koboShelf1);
|
||||||
|
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(List.of(settings1, settings2));
|
||||||
|
when(shelfRepository.findByUserIdInAndName(List.of(100L, 200L), ShelfType.KOBO.getName()))
|
||||||
|
.thenReturn(List.of(koboShelf1, koboShelf2));
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository).save(testBook);
|
||||||
|
assert testBook.getShelves().contains(koboShelf1);
|
||||||
|
assert testBook.getShelves().contains(koboShelf2);
|
||||||
|
assert testBook.getShelves().size() == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withBookOnAllShelves_shouldNotSave() {
|
||||||
|
testBook.getShelves().add(koboShelf1);
|
||||||
|
testBook.getShelves().add(koboShelf2);
|
||||||
|
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(List.of(settings1, settings2));
|
||||||
|
when(shelfRepository.findByUserIdInAndName(List.of(100L, 200L), ShelfType.KOBO.getName()))
|
||||||
|
.thenReturn(List.of(koboShelf1, koboShelf2));
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository, never()).save(any());
|
||||||
|
assert testBook.getShelves().size() == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withNoKoboShelvesFound_shouldNotSave() {
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(List.of(settings1, settings2));
|
||||||
|
when(shelfRepository.findByUserIdInAndName(List.of(100L, 200L), ShelfType.KOBO.getName()))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(shelfRepository).findByUserIdInAndName(List.of(100L, 200L), ShelfType.KOBO.getName());
|
||||||
|
verify(bookRepository, never()).save(any());
|
||||||
|
assert testBook.getShelves().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withSingleUser_shouldAddToSingleShelf() {
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(List.of(settings1));
|
||||||
|
when(shelfRepository.findByUserIdInAndName(List.of(100L), ShelfType.KOBO.getName()))
|
||||||
|
.thenReturn(List.of(koboShelf1));
|
||||||
|
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
|
||||||
|
verify(bookRepository).save(testBook);
|
||||||
|
assert testBook.getShelves().contains(koboShelf1);
|
||||||
|
assert testBook.getShelves().size() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void autoAddBookToKoboShelves_withNullShelves_shouldInitializeAndAdd() {
|
||||||
|
testBook = BookEntity.builder()
|
||||||
|
.id(1L)
|
||||||
|
.shelves(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
|
||||||
|
when(koboCompatibilityService.isBookSupportedForKobo(testBook)).thenReturn(true);
|
||||||
|
when(koboUserSettingsRepository.findByAutoAddToShelfTrueAndSyncEnabledTrue())
|
||||||
|
.thenReturn(List.of(settings1));
|
||||||
|
when(shelfRepository.findByUserIdInAndName(List.of(100L), ShelfType.KOBO.getName()))
|
||||||
|
.thenReturn(List.of(koboShelf1));
|
||||||
|
|
||||||
|
try {
|
||||||
|
koboAutoShelfService.autoAddBookToKoboShelves(1L);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package com.adityachandel.booklore.service.kobo;
|
||||||
|
|
||||||
|
import com.adityachandel.booklore.config.security.service.AuthenticationService;
|
||||||
|
import com.adityachandel.booklore.model.dto.BookLoreUser;
|
||||||
|
import com.adityachandel.booklore.model.dto.KoboSyncSettings;
|
||||||
|
import com.adityachandel.booklore.model.dto.Shelf;
|
||||||
|
import com.adityachandel.booklore.model.dto.request.ShelfCreateRequest;
|
||||||
|
import com.adityachandel.booklore.model.entity.KoboUserSettingsEntity;
|
||||||
|
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
||||||
|
import com.adityachandel.booklore.model.enums.IconType;
|
||||||
|
import com.adityachandel.booklore.model.enums.ShelfType;
|
||||||
|
import com.adityachandel.booklore.repository.KoboUserSettingsRepository;
|
||||||
|
import com.adityachandel.booklore.service.ShelfService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.*;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class KoboSettingsServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KoboUserSettingsRepository repository;
|
||||||
|
@Mock
|
||||||
|
private AuthenticationService authenticationService;
|
||||||
|
@Mock
|
||||||
|
private ShelfService shelfService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private KoboSettingsService service;
|
||||||
|
|
||||||
|
private BookLoreUser user;
|
||||||
|
private KoboUserSettingsEntity settingsEntity;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
user = BookLoreUser.builder().id(1L).build();
|
||||||
|
settingsEntity = KoboUserSettingsEntity.builder()
|
||||||
|
.id(10L)
|
||||||
|
.userId(1L)
|
||||||
|
.token("token")
|
||||||
|
.syncEnabled(true)
|
||||||
|
.autoAddToShelf(true)
|
||||||
|
.progressMarkAsReadingThreshold(0.5f)
|
||||||
|
.progressMarkAsFinishedThreshold(0.9f)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCurrentUserSettings_existingSettings() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.getCurrentUserSettings();
|
||||||
|
|
||||||
|
assertEquals(settingsEntity.getId(), dto.getId());
|
||||||
|
assertEquals(settingsEntity.getUserId().toString(), dto.getUserId());
|
||||||
|
assertEquals(settingsEntity.getToken(), dto.getToken());
|
||||||
|
assertTrue(dto.isSyncEnabled());
|
||||||
|
assertTrue(dto.isAutoAddToShelf());
|
||||||
|
assertEquals(settingsEntity.getProgressMarkAsReadingThreshold(), dto.getProgressMarkAsReadingThreshold());
|
||||||
|
assertEquals(settingsEntity.getProgressMarkAsFinishedThreshold(), dto.getProgressMarkAsFinishedThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCurrentUserSettings_noSettings_createsDefault() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.empty());
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName()))).thenReturn(Optional.empty());
|
||||||
|
doReturn(Shelf.builder().id(100L).build()).when(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.getCurrentUserSettings();
|
||||||
|
|
||||||
|
assertEquals(user.getId().toString(), dto.getUserId());
|
||||||
|
assertNotNull(dto.getToken());
|
||||||
|
assertFalse(dto.isSyncEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createOrUpdateToken_existingSettings() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName()))).thenReturn(Optional.of(ShelfEntity.builder().id(100L).build()));
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.createOrUpdateToken();
|
||||||
|
|
||||||
|
assertEquals(settingsEntity.getUserId().toString(), dto.getUserId());
|
||||||
|
assertNotNull(dto.getToken());
|
||||||
|
assertNotEquals("token", dto.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createOrUpdateToken_noSettings_createsNew() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.empty());
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName()))).thenReturn(Optional.empty());
|
||||||
|
doReturn(Shelf.builder().id(100L).build()).when(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.createOrUpdateToken();
|
||||||
|
|
||||||
|
assertEquals(user.getId().toString(), dto.getUserId());
|
||||||
|
assertNotNull(dto.getToken());
|
||||||
|
assertFalse(dto.isSyncEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateSettings_disableSync_deletesShelf() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
Shelf shelf = Shelf.builder().id(100L).build();
|
||||||
|
when(shelfService.getUserKoboShelf()).thenReturn(shelf);
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
KoboSyncSettings update = new KoboSyncSettings();
|
||||||
|
update.setSyncEnabled(false);
|
||||||
|
update.setAutoAddToShelf(false);
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.updateSettings(update);
|
||||||
|
|
||||||
|
verify(shelfService).deleteShelf(100L);
|
||||||
|
assertFalse(dto.isSyncEnabled());
|
||||||
|
assertFalse(dto.isAutoAddToShelf());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateSettings_enableSync_createsShelf() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
settingsEntity.setSyncEnabled(false);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
when(shelfService.getUserKoboShelf()).thenReturn(null);
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName()))).thenReturn(Optional.empty());
|
||||||
|
doReturn(Shelf.builder().id(100L).build()).when(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
|
||||||
|
KoboSyncSettings update = new KoboSyncSettings();
|
||||||
|
update.setSyncEnabled(true);
|
||||||
|
update.setAutoAddToShelf(true);
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.updateSettings(update);
|
||||||
|
|
||||||
|
verify(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
assertTrue(dto.isSyncEnabled());
|
||||||
|
assertTrue(dto.isAutoAddToShelf());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateSettings_updatesThresholds() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
KoboSyncSettings update = new KoboSyncSettings();
|
||||||
|
update.setSyncEnabled(true);
|
||||||
|
update.setAutoAddToShelf(true);
|
||||||
|
update.setProgressMarkAsReadingThreshold(0.7f);
|
||||||
|
update.setProgressMarkAsFinishedThreshold(0.95f);
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.updateSettings(update);
|
||||||
|
|
||||||
|
assertEquals((Float)0.7f, dto.getProgressMarkAsReadingThreshold());
|
||||||
|
assertEquals((Float)0.95f, dto.getProgressMarkAsFinishedThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateSettings_nullThresholds_shouldNotChangeExisting() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
Float originalReading = settingsEntity.getProgressMarkAsReadingThreshold();
|
||||||
|
Float originalFinished = settingsEntity.getProgressMarkAsFinishedThreshold();
|
||||||
|
|
||||||
|
KoboSyncSettings update = new KoboSyncSettings();
|
||||||
|
update.setSyncEnabled(true);
|
||||||
|
update.setAutoAddToShelf(true);
|
||||||
|
update.setProgressMarkAsReadingThreshold(null);
|
||||||
|
update.setProgressMarkAsFinishedThreshold(null);
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.updateSettings(update);
|
||||||
|
|
||||||
|
assertEquals(originalReading, dto.getProgressMarkAsReadingThreshold());
|
||||||
|
assertEquals(originalFinished, dto.getProgressMarkAsFinishedThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCurrentUserSettings_settingsWithNullToken_shouldReturnDtoWithNullToken() {
|
||||||
|
settingsEntity.setToken(null);
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
|
||||||
|
KoboSyncSettings dto = service.getCurrentUserSettings();
|
||||||
|
|
||||||
|
assertNull(dto.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCurrentUserSettings_noAuthenticatedUser_shouldThrowException() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(null);
|
||||||
|
assertThrows(NullPointerException.class, () -> service.getCurrentUserSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateSettings_getUserKoboShelfReturnsNull_shouldNotThrow() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L)).thenReturn(Optional.of(settingsEntity));
|
||||||
|
when(shelfService.getUserKoboShelf()).thenReturn(null);
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
KoboSyncSettings update = new KoboSyncSettings();
|
||||||
|
update.setSyncEnabled(false);
|
||||||
|
update.setAutoAddToShelf(false);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> service.updateSettings(update));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ensureKoboShelfExists_doesNotCreateIfExists() throws Exception {
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName()))).thenReturn(Optional.of(ShelfEntity.builder().id(100L).build()));
|
||||||
|
|
||||||
|
var method = service.getClass().getDeclaredMethod("ensureKoboShelfExists", Long.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
assertDoesNotThrow(() -> method.invoke(service, 1L));
|
||||||
|
verify(shelfService, never()).createShelf(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ensureKoboShelfExists_createsIfMissing() throws Exception {
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName()))).thenReturn(Optional.empty());
|
||||||
|
doReturn(Shelf.builder().id(100L).build()).when(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
|
||||||
|
var method = service.getClass().getDeclaredMethod("ensureKoboShelfExists", Long.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
assertDoesNotThrow(() -> method.invoke(service, 1L));
|
||||||
|
verify(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ensureKoboShelfExists_idempotentIfCalledTwice() throws Exception {
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName())))
|
||||||
|
.thenReturn(Optional.empty())
|
||||||
|
.thenReturn(Optional.of(ShelfEntity.builder().id(100L).build()));
|
||||||
|
doReturn(Shelf.builder().id(100L).build()).when(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
|
||||||
|
var method = service.getClass().getDeclaredMethod("ensureKoboShelfExists", Long.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
|
||||||
|
method.invoke(service, 1L);
|
||||||
|
method.invoke(service, 1L);
|
||||||
|
|
||||||
|
verify(shelfService, times(1)).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createOrUpdateToken_multipleCalls_generateDifferentTokens() {
|
||||||
|
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||||
|
when(repository.findByUserId(1L))
|
||||||
|
.thenReturn(Optional.empty())
|
||||||
|
.thenReturn(Optional.of(settingsEntity));
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(shelfService.getShelf(eq(1L), eq(ShelfType.KOBO.getName()))).thenReturn(Optional.empty());
|
||||||
|
doReturn(Shelf.builder().id(100L).build()).when(shelfService).createShelf(any(ShelfCreateRequest.class));
|
||||||
|
|
||||||
|
KoboSyncSettings dto1 = service.createOrUpdateToken();
|
||||||
|
// Simulate a new call with an existing entity
|
||||||
|
KoboSyncSettings dto2 = service.createOrUpdateToken();
|
||||||
|
|
||||||
|
assertNotEquals(dto1.getToken(), dto2.getToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (koboSyncSettings.syncEnabled) {
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<div class="setting-label-row">
|
||||||
|
<label class="setting-label">Auto-Add New Books to Kobo Shelf</label>
|
||||||
|
<p-toggle-switch
|
||||||
|
id="autoAddToShelf"
|
||||||
|
name="autoAddToShelf"
|
||||||
|
[(ngModel)]="koboSyncSettings.autoAddToShelf"
|
||||||
|
(ngModelChange)="onAutoAddToggle()">
|
||||||
|
</p-toggle-switch>
|
||||||
|
</div>
|
||||||
|
<p class="setting-description">
|
||||||
|
When enabled, newly added books will be automatically added to your Kobo shelf for syncing.
|
||||||
|
This eliminates the need to manually add each book to the shelf.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (koboSyncSettings.syncEnabled) {
|
@if (koboSyncSettings.syncEnabled) {
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly destroy$ = new Subject<void>();
|
private readonly destroy$ = new Subject<void>();
|
||||||
private readonly sliderChange$ = new Subject<void>();
|
private readonly sliderChange$ = new Subject<void>();
|
||||||
|
private readonly progressThresholdChange$ = new Subject<void>();
|
||||||
|
|
||||||
hasKoboTokenPermission = false;
|
hasKoboTokenPermission = false;
|
||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
@@ -55,7 +56,8 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
|
|||||||
token: '',
|
token: '',
|
||||||
syncEnabled: false,
|
syncEnabled: false,
|
||||||
progressMarkAsReadingThreshold: 1,
|
progressMarkAsReadingThreshold: 1,
|
||||||
progressMarkAsFinishedThreshold: 99
|
progressMarkAsFinishedThreshold: 99,
|
||||||
|
autoAddToShelf: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -70,6 +72,13 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe(() => {
|
).subscribe(() => {
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.progressThresholdChange$.pipe(
|
||||||
|
debounceTime(500),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(() => {
|
||||||
|
this.updateKoboSettings('Progress thresholds updated successfully');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupUserStateSubscription() {
|
private setupUserStateSubscription() {
|
||||||
@@ -108,6 +117,7 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
|
|||||||
this.koboSyncSettings.syncEnabled = settings.syncEnabled;
|
this.koboSyncSettings.syncEnabled = settings.syncEnabled;
|
||||||
this.koboSyncSettings.progressMarkAsReadingThreshold = settings.progressMarkAsReadingThreshold ?? 1;
|
this.koboSyncSettings.progressMarkAsReadingThreshold = settings.progressMarkAsReadingThreshold ?? 1;
|
||||||
this.koboSyncSettings.progressMarkAsFinishedThreshold = settings.progressMarkAsFinishedThreshold ?? 99;
|
this.koboSyncSettings.progressMarkAsFinishedThreshold = settings.progressMarkAsFinishedThreshold ?? 99;
|
||||||
|
this.koboSyncSettings.autoAddToShelf = settings.autoAddToShelf ?? false;
|
||||||
this.credentialsSaved = !!settings.token;
|
this.credentialsSaved = !!settings.token;
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -191,55 +201,44 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
|
|||||||
message: 'Disabling Kobo sync will delete your Kobo shelf. Are you sure you want to proceed?',
|
message: 'Disabling Kobo sync will delete your Kobo shelf. Are you sure you want to proceed?',
|
||||||
header: 'Confirm Disable',
|
header: 'Confirm Disable',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
icon: 'pi pi-exclamation-triangle',
|
||||||
accept: () => this.performToggle(false),
|
accept: () => this.updateKoboSettings('Kobo sync disabled'),
|
||||||
reject: () => {
|
reject: () => {
|
||||||
this.koboSyncSettings.syncEnabled = true;
|
this.koboSyncSettings.syncEnabled = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.performToggle(true);
|
this.updateKoboSettings('Kobo sync enabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private performToggle(enabled: boolean) {
|
onProgressThresholdsChange() {
|
||||||
this.koboService.toggleSync(enabled).subscribe({
|
this.progressThresholdChange$.next();
|
||||||
next: () => {
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Sync Updated',
|
|
||||||
detail: enabled
|
|
||||||
? 'Kobo sync enabled'
|
|
||||||
: 'Kobo sync disabled'
|
|
||||||
});
|
|
||||||
this.shelfService.reloadShelves();
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: 'Failed to update sync setting'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgressThresholdsChange() {
|
onAutoAddToggle() {
|
||||||
this.koboService.updateProgressThresholds(
|
const message = this.koboSyncSettings.autoAddToShelf
|
||||||
this.koboSyncSettings.progressMarkAsReadingThreshold,
|
? 'New books will be automatically added to Kobo shelf'
|
||||||
this.koboSyncSettings.progressMarkAsFinishedThreshold
|
: 'Auto-add to Kobo shelf disabled';
|
||||||
).subscribe({
|
this.updateKoboSettings(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateKoboSettings(successMessage: string) {
|
||||||
|
this.koboService.updateSettings(this.koboSyncSettings).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.messageService.add({
|
this.messageService.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Thresholds Updated',
|
summary: 'Settings Updated',
|
||||||
detail: 'Progress thresholds updated successfully'
|
detail: successMessage
|
||||||
});
|
});
|
||||||
|
if (!this.koboSyncSettings.syncEnabled) {
|
||||||
|
this.shelfService.reloadShelves();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.messageService.add({
|
this.messageService.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: 'Error',
|
summary: 'Error',
|
||||||
detail: 'Failed to update progress thresholds'
|
detail: 'Failed to update Kobo settings'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface KoboSyncSettings {
|
|||||||
syncEnabled: boolean;
|
syncEnabled: boolean;
|
||||||
progressMarkAsReadingThreshold?: number;
|
progressMarkAsReadingThreshold?: number;
|
||||||
progressMarkAsFinishedThreshold?: number;
|
progressMarkAsFinishedThreshold?: number;
|
||||||
|
autoAddToShelf: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -22,22 +23,10 @@ export class KoboService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createOrUpdateToken(): Observable<KoboSyncSettings> {
|
createOrUpdateToken(): Observable<KoboSyncSettings> {
|
||||||
return this.http.put<KoboSyncSettings>(`${this.baseUrl}`, null);
|
return this.http.put<KoboSyncSettings>(`${this.baseUrl}/token`, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSync(enabled: boolean): Observable<void> {
|
updateSettings(settings: KoboSyncSettings): Observable<KoboSyncSettings> {
|
||||||
const params = new HttpParams().set('enabled', enabled.toString());
|
return this.http.put<KoboSyncSettings>(`${this.baseUrl}`, settings);
|
||||||
return this.http.put<void>(`${this.baseUrl}/sync`, null, { params });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgressThresholds(readingThreshold?: number, finishedThreshold?: number): Observable<KoboSyncSettings> {
|
|
||||||
let params = new HttpParams();
|
|
||||||
if (readingThreshold !== undefined && readingThreshold !== null) {
|
|
||||||
params = params.set('readingThreshold', readingThreshold.toString());
|
|
||||||
}
|
|
||||||
if (finishedThreshold !== undefined && finishedThreshold !== null) {
|
|
||||||
params = params.set('finishedThreshold', finishedThreshold.toString());
|
|
||||||
}
|
|
||||||
return this.http.put<KoboSyncSettings>(`${this.baseUrl}/progress-thresholds`, null, { params });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user