feat(icons): Made icons for libraries and shelves optional with no default icons. (#2599)

* feat(icons): Made icons for libraries and shelves optional with no default icon displayed.

* Added tests for making sure the API handles nullable icons for Shelves and Libraries.

* Fixed some issues identified during PR review.

* Rebased on develop.
This commit is contained in:
Giroux Arthur
2026-02-07 08:25:34 -07:00
committed by GitHub
parent 07cbf89e4c
commit 14236299f2
28 changed files with 817 additions and 90 deletions

View File

@@ -15,7 +15,6 @@ public class MagicShelf {
@Size(max = 255, message = "Shelf name must not exceed 255 characters")
private String name;
@NotBlank(message = "Icon must not be blank")
@Size(max = 64, message = "Icon must not exceed 64 characters")
private String icon;

View File

@@ -19,10 +19,7 @@ public class CreateLibraryRequest {
@NotBlank(message = "Library name must not be empty.")
private String name;
@NotBlank(message = "Library icon must not be empty.")
private String icon;
@NotNull(message = "Library icon type must not be null.")
private IconType iconType;
@NotEmpty(message = "Library paths must not be empty.")

View File

@@ -16,10 +16,7 @@ public class ShelfCreateRequest {
@NotBlank(message = "Shelf name must not be empty.")
private String name;
@NotBlank(message = "Shelf icon must not be empty.")
private String icon;
@NotNull(message = "Shelf icon type must not be null.")
private IconType iconType;
private boolean publicShelf;

View File

@@ -44,9 +44,8 @@ public class LibraryEntity {
private String icon;
@Enumerated(EnumType.STRING)
@Column(name = "icon_type", nullable = false)
@Builder.Default
private IconType iconType = IconType.PRIME_NG;
@Column(name = "icon_type")
private IconType iconType;
@Column(name = "file_naming_pattern")
private String fileNamingPattern;
@@ -65,10 +64,4 @@ public class LibraryEntity {
@Builder.Default
private LibraryOrganizationMode organizationMode = LibraryOrganizationMode.AUTO_DETECT;
@PrePersist
public void ensureIconType() {
if (this.iconType == null) {
this.iconType = IconType.PRIME_NG;
}
}
}

View File

@@ -27,13 +27,11 @@ public class MagicShelfEntity {
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String icon;
@Enumerated(EnumType.STRING)
@Column(name = "icon_type", nullable = false)
@Builder.Default
private IconType iconType = IconType.PRIME_NG;
@Column(name = "icon_type")
private IconType iconType;
@Column(name = "filter_json", columnDefinition = "json", nullable = false)
private String filterJson;
@@ -54,11 +52,4 @@ public class MagicShelfEntity {
public void onUpdate() {
updatedAt = LocalDateTime.now();
}
@PrePersist
public void ensureIconType() {
if (this.iconType == null) {
this.iconType = IconType.PRIME_NG;
}
}
}

View File

@@ -36,9 +36,8 @@ public class ShelfEntity {
private String icon;
@Enumerated(EnumType.STRING)
@Column(name = "icon_type", nullable = false)
@Builder.Default
private IconType iconType = IconType.PRIME_NG;
@Column(name = "icon_type")
private IconType iconType;
@Column(name = "is_public", nullable = false)
@Builder.Default

View File

@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.booklore.model.enums.IconType;
@Slf4j
@Service
@@ -24,6 +25,7 @@ public class UserDefaultsService {
.user(user)
.name("Favorites")
.icon("heart")
.iconType(IconType.PRIME_NG)
.build();
shelfRepository.save(shelf);
}

View File

@@ -0,0 +1,8 @@
ALTER TABLE library MODIFY COLUMN icon VARCHAR(64) NULL;
ALTER TABLE library MODIFY COLUMN icon_type VARCHAR(255) NULL;
ALTER TABLE magic_shelf MODIFY COLUMN icon VARCHAR(64) NULL;
ALTER TABLE magic_shelf MODIFY COLUMN icon_type VARCHAR(255) NULL;
ALTER TABLE shelf MODIFY COLUMN icon VARCHAR(64) NULL;
ALTER TABLE shelf MODIFY COLUMN icon_type VARCHAR(255) NULL;

View File

@@ -0,0 +1,191 @@
package org.booklore.service;
import org.booklore.config.security.service.AuthenticationService;
import org.booklore.model.dto.BookLoreUser;
import org.booklore.model.dto.MagicShelf;
import org.booklore.model.entity.MagicShelfEntity;
import org.booklore.model.enums.IconType;
import org.booklore.repository.MagicShelfRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MagicShelfServiceTest {
@Mock
private MagicShelfRepository magicShelfRepository;
@Mock
private AuthenticationService authenticationService;
@InjectMocks
private MagicShelfService magicShelfService;
private BookLoreUser user;
@BeforeEach
void setUp() {
BookLoreUser.UserPermissions permissions = new BookLoreUser.UserPermissions();
permissions.setAdmin(true);
user = BookLoreUser.builder().id(1L).isDefaultPassword(false).permissions(permissions).build();
}
@Test
void createShelf_withNullIcon_shouldPersistNullIconValues() {
MagicShelf dto = new MagicShelf();
dto.setName("Unread Books");
dto.setIcon(null);
dto.setIconType(null);
dto.setFilterJson("{\"status\": \"unread\"}");
dto.setIsPublic(false);
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(magicShelfRepository.existsByUserIdAndName(1L, "Unread Books")).thenReturn(false);
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> {
MagicShelfEntity entity = invocation.getArgument(0);
entity.setId(1L);
return entity;
});
MagicShelf result = magicShelfService.createOrUpdateShelf(dto);
ArgumentCaptor<MagicShelfEntity> captor = ArgumentCaptor.forClass(MagicShelfEntity.class);
verify(magicShelfRepository).save(captor.capture());
MagicShelfEntity saved = captor.getValue();
assertNull(saved.getIcon());
assertNull(saved.getIconType());
assertEquals("Unread Books", saved.getName());
}
@Test
void createShelf_withIcon_shouldPersistIconValues() {
MagicShelf dto = new MagicShelf();
dto.setName("Favorites");
dto.setIcon("star");
dto.setIconType(IconType.PRIME_NG);
dto.setFilterJson("{\"rating\": 5}");
dto.setIsPublic(false);
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(magicShelfRepository.existsByUserIdAndName(1L, "Favorites")).thenReturn(false);
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> {
MagicShelfEntity entity = invocation.getArgument(0);
entity.setId(1L);
return entity;
});
MagicShelf result = magicShelfService.createOrUpdateShelf(dto);
assertNotNull(result);
assertEquals("star", result.getIcon());
assertEquals(IconType.PRIME_NG, result.getIconType());
}
@Test
void updateShelf_withNullIcon_shouldClearIconValues() {
MagicShelfEntity existing = MagicShelfEntity.builder()
.id(1L)
.userId(1L)
.name("Old Shelf")
.icon("star")
.iconType(IconType.PRIME_NG)
.filterJson("{\"status\": \"reading\"}")
.build();
MagicShelf dto = new MagicShelf();
dto.setId(1L);
dto.setName("Updated Shelf");
dto.setIcon(null);
dto.setIconType(null);
dto.setFilterJson("{\"status\": \"updated\"}");
dto.setIsPublic(false);
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(magicShelfRepository.findById(1L)).thenReturn(Optional.of(existing));
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
magicShelfService.createOrUpdateShelf(dto);
ArgumentCaptor<MagicShelfEntity> captor = ArgumentCaptor.forClass(MagicShelfEntity.class);
verify(magicShelfRepository).save(captor.capture());
MagicShelfEntity saved = captor.getValue();
assertNull(saved.getIcon());
assertNull(saved.getIconType());
assertEquals("Updated Shelf", saved.getName());
}
@Test
void updateShelf_withIcon_shouldPreserveIconValues() {
MagicShelfEntity existing = MagicShelfEntity.builder()
.id(1L)
.userId(1L)
.name("Old Shelf")
.icon("star")
.iconType(IconType.PRIME_NG)
.filterJson("{\"status\": \"reading\"}")
.build();
MagicShelf dto = new MagicShelf();
dto.setId(1L);
dto.setName("Updated Shelf");
dto.setIcon("bookmark");
dto.setIconType(IconType.CUSTOM_SVG);
dto.setFilterJson("{\"status\": \"updated\"}");
dto.setIsPublic(false);
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(magicShelfRepository.findById(1L)).thenReturn(Optional.of(existing));
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
magicShelfService.createOrUpdateShelf(dto);
ArgumentCaptor<MagicShelfEntity> captor = ArgumentCaptor.forClass(MagicShelfEntity.class);
verify(magicShelfRepository).save(captor.capture());
MagicShelfEntity saved = captor.getValue();
assertEquals("bookmark", saved.getIcon());
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
}
@Test
void updateShelf_fromIconToNull_shouldAllowRemovingIcon() {
MagicShelfEntity existing = MagicShelfEntity.builder()
.id(1L)
.userId(1L)
.name("Shelf With Icon")
.icon("heart")
.iconType(IconType.PRIME_NG)
.filterJson("{}")
.build();
MagicShelf dto = new MagicShelf();
dto.setId(1L);
dto.setName("Shelf With Icon");
dto.setIcon(null);
dto.setIconType(null);
dto.setFilterJson("{}");
dto.setIsPublic(false);
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(magicShelfRepository.findById(1L)).thenReturn(Optional.of(existing));
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
MagicShelf result = magicShelfService.createOrUpdateShelf(dto);
assertNull(result.getIcon());
assertNull(result.getIconType());
}
}

View File

@@ -0,0 +1,190 @@
package org.booklore.service;
import org.booklore.config.security.service.AuthenticationService;
import org.booklore.mapper.ShelfMapper;
import org.booklore.model.dto.BookLoreUser;
import org.booklore.model.dto.Shelf;
import org.booklore.model.dto.request.ShelfCreateRequest;
import org.booklore.model.entity.BookLoreUserEntity;
import org.booklore.model.entity.ShelfEntity;
import org.booklore.model.enums.IconType;
import org.booklore.repository.BookRepository;
import org.booklore.repository.ShelfRepository;
import org.booklore.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ShelfServiceTest {
@Mock
private ShelfRepository shelfRepository;
@Mock
private BookRepository bookRepository;
@Mock
private ShelfMapper shelfMapper;
@Mock
private AuthenticationService authenticationService;
@Mock
private UserRepository userRepository;
@InjectMocks
private ShelfService shelfService;
private BookLoreUser user;
private BookLoreUserEntity userEntity;
@BeforeEach
void setUp() {
user = BookLoreUser.builder().id(1L).isDefaultPassword(false).build();
userEntity = BookLoreUserEntity.builder().id(1L).username("testuser").build();
}
@Test
void createShelf_withNullIcon_shouldPersistNullIconValues() {
ShelfCreateRequest request = ShelfCreateRequest.builder()
.name("My Shelf")
.icon(null)
.iconType(null)
.build();
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(shelfRepository.existsByUserIdAndName(1L, "My Shelf")).thenReturn(false);
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(Shelf.builder().name("My Shelf").build());
shelfService.createShelf(request);
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
verify(shelfRepository).save(captor.capture());
ShelfEntity saved = captor.getValue();
assertNull(saved.getIcon());
assertNull(saved.getIconType());
assertEquals("My Shelf", saved.getName());
}
@Test
void createShelf_withIcon_shouldPersistIconValues() {
ShelfCreateRequest request = ShelfCreateRequest.builder()
.name("My Shelf")
.icon("heart")
.iconType(IconType.PRIME_NG)
.build();
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(shelfRepository.existsByUserIdAndName(1L, "My Shelf")).thenReturn(false);
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(
Shelf.builder().name("My Shelf").icon("heart").iconType(IconType.PRIME_NG).build());
shelfService.createShelf(request);
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
verify(shelfRepository).save(captor.capture());
ShelfEntity saved = captor.getValue();
assertEquals("heart", saved.getIcon());
assertEquals(IconType.PRIME_NG, saved.getIconType());
}
@Test
void updateShelf_withNullIcon_shouldClearIconValues() {
ShelfEntity existingShelf = ShelfEntity.builder()
.id(1L)
.name("Old Shelf")
.icon("star")
.iconType(IconType.PRIME_NG)
.user(userEntity)
.build();
ShelfCreateRequest request = ShelfCreateRequest.builder()
.name("Updated Shelf")
.icon(null)
.iconType(null)
.build();
when(shelfRepository.findById(1L)).thenReturn(Optional.of(existingShelf));
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(Shelf.builder().name("Updated Shelf").build());
shelfService.updateShelf(1L, request);
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
verify(shelfRepository).save(captor.capture());
ShelfEntity saved = captor.getValue();
assertNull(saved.getIcon());
assertNull(saved.getIconType());
assertEquals("Updated Shelf", saved.getName());
}
@Test
void updateShelf_withIcon_shouldPreserveIconValues() {
ShelfEntity existingShelf = ShelfEntity.builder()
.id(1L)
.name("Old Shelf")
.icon("star")
.iconType(IconType.PRIME_NG)
.user(userEntity)
.build();
ShelfCreateRequest request = ShelfCreateRequest.builder()
.name("Updated Shelf")
.icon("bookmark")
.iconType(IconType.CUSTOM_SVG)
.build();
when(shelfRepository.findById(1L)).thenReturn(Optional.of(existingShelf));
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(
Shelf.builder().name("Updated Shelf").icon("bookmark").iconType(IconType.CUSTOM_SVG).build());
shelfService.updateShelf(1L, request);
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
verify(shelfRepository).save(captor.capture());
ShelfEntity saved = captor.getValue();
assertEquals("bookmark", saved.getIcon());
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
}
@Test
void createShelf_withCustomSvgIcon_shouldPersistCorrectIconType() {
ShelfCreateRequest request = ShelfCreateRequest.builder()
.name("SVG Shelf")
.icon("<svg>...</svg>")
.iconType(IconType.CUSTOM_SVG)
.build();
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(shelfRepository.existsByUserIdAndName(1L, "SVG Shelf")).thenReturn(false);
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(
Shelf.builder().name("SVG Shelf").icon("<svg>...</svg>").iconType(IconType.CUSTOM_SVG).build());
shelfService.createShelf(request);
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
verify(shelfRepository).save(captor.capture());
ShelfEntity saved = captor.getValue();
assertEquals("<svg>...</svg>", saved.getIcon());
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
}
}

View File

@@ -0,0 +1,236 @@
package org.booklore.service.library;
import org.booklore.config.security.service.AuthenticationService;
import org.booklore.mapper.LibraryMapper;
import org.booklore.model.dto.BookLoreUser;
import org.booklore.model.dto.Library;
import org.booklore.model.dto.LibraryPath;
import org.booklore.model.dto.request.CreateLibraryRequest;
import org.booklore.model.entity.LibraryEntity;
import org.booklore.model.entity.LibraryPathEntity;
import org.booklore.model.enums.IconType;
import org.booklore.repository.LibraryRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.booklore.mapper.BookMapper;
import org.booklore.repository.BookRepository;
import org.booklore.repository.LibraryPathRepository;
import org.booklore.repository.UserRepository;
import org.booklore.model.entity.BookLoreUserEntity;
import org.booklore.service.NotificationService;
import org.booklore.service.monitoring.MonitoringService;
import org.booklore.util.FileService;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class LibraryServiceIconTest {
@Mock
private LibraryRepository libraryRepository;
@Mock
private LibraryPathRepository libraryPathRepository;
@Mock
private BookRepository bookRepository;
@Mock
private LibraryProcessingService libraryProcessingService;
@Mock
private BookMapper bookMapper;
@Mock
private LibraryMapper libraryMapper;
@Mock
private NotificationService notificationService;
@Mock
private FileService fileService;
@Mock
private MonitoringService monitoringService;
@Mock
private AuthenticationService authenticationService;
@Mock
private UserRepository userRepository;
@InjectMocks
private LibraryService libraryService;
private BookLoreUser user;
private BookLoreUserEntity userEntity;
@BeforeEach
void setUp() {
user = BookLoreUser.builder().id(1L).isDefaultPassword(false).build();
userEntity = BookLoreUserEntity.builder().id(1L).username("testuser").build();
}
@Test
void updateLibrary_withNullIcon_shouldClearIconValues() {
LibraryEntity existing = LibraryEntity.builder()
.id(1L)
.name("My Library")
.icon("book")
.iconType(IconType.PRIME_NG)
.libraryPaths(new ArrayList<>())
.watch(false)
.build();
CreateLibraryRequest request = CreateLibraryRequest.builder()
.name("Updated Library")
.icon(null)
.iconType(null)
.paths(Collections.emptyList())
.watch(false)
.build();
when(libraryRepository.findById(1L)).thenReturn(Optional.of(existing));
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(Library.builder().name("Updated Library").build());
libraryService.updateLibrary(request, 1L);
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
verify(libraryRepository).save(captor.capture());
LibraryEntity saved = captor.getValue();
assertNull(saved.getIcon());
assertNull(saved.getIconType());
assertEquals("Updated Library", saved.getName());
}
@Test
void updateLibrary_withIcon_shouldPreserveIconValues() {
LibraryEntity existing = LibraryEntity.builder()
.id(1L)
.name("My Library")
.icon("book")
.iconType(IconType.PRIME_NG)
.libraryPaths(new ArrayList<>())
.watch(false)
.build();
CreateLibraryRequest request = CreateLibraryRequest.builder()
.name("Updated Library")
.icon("folder")
.iconType(IconType.CUSTOM_SVG)
.paths(Collections.emptyList())
.watch(false)
.build();
when(libraryRepository.findById(1L)).thenReturn(Optional.of(existing));
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(
Library.builder().name("Updated Library").icon("folder").iconType(IconType.CUSTOM_SVG).build());
libraryService.updateLibrary(request, 1L);
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
verify(libraryRepository).save(captor.capture());
LibraryEntity saved = captor.getValue();
assertEquals("folder", saved.getIcon());
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
}
@Test
void updateLibrary_fromIconToNull_shouldAllowRemovingIcon() {
LibraryEntity existing = LibraryEntity.builder()
.id(1L)
.name("Library With Icon")
.icon("star")
.iconType(IconType.PRIME_NG)
.libraryPaths(new ArrayList<>())
.watch(false)
.build();
CreateLibraryRequest request = CreateLibraryRequest.builder()
.name("Library With Icon")
.icon(null)
.iconType(null)
.paths(Collections.emptyList())
.watch(false)
.build();
when(libraryRepository.findById(1L)).thenReturn(Optional.of(existing));
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(Library.builder().name("Library With Icon").build());
libraryService.updateLibrary(request, 1L);
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
verify(libraryRepository).save(captor.capture());
LibraryEntity saved = captor.getValue();
assertNull(saved.getIcon());
assertNull(saved.getIconType());
}
@Test
void createLibrary_withNullIcon_shouldPersistNullIconValues() {
CreateLibraryRequest request = CreateLibraryRequest.builder()
.name("No Icon Library")
.icon(null)
.iconType(null)
.paths(Collections.emptyList())
.watch(false)
.build();
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> {
LibraryEntity entity = invocation.getArgument(0);
entity.setId(1L);
return entity;
});
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(Library.builder().name("No Icon Library").build());
libraryService.createLibrary(request);
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
verify(libraryRepository).save(captor.capture());
LibraryEntity saved = captor.getValue();
assertNull(saved.getIcon());
assertNull(saved.getIconType());
assertEquals("No Icon Library", saved.getName());
}
@Test
void createLibrary_withIcon_shouldPersistIconValues() {
CreateLibraryRequest request = CreateLibraryRequest.builder()
.name("Icon Library")
.icon("book")
.iconType(IconType.PRIME_NG)
.paths(Collections.emptyList())
.watch(false)
.build();
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> {
LibraryEntity entity = invocation.getArgument(0);
entity.setId(1L);
return entity;
});
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(
Library.builder().name("Icon Library").icon("book").iconType(IconType.PRIME_NG).build());
libraryService.createLibrary(request);
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
verify(libraryRepository).save(captor.capture());
LibraryEntity saved = captor.getValue();
assertEquals("book", saved.getIcon());
assertEquals(IconType.PRIME_NG, saved.getIconType());
}
}

View File

@@ -0,0 +1,71 @@
package org.booklore.service.user;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.booklore.model.entity.BookLoreUserEntity;
import org.booklore.model.entity.ShelfEntity;
import org.booklore.model.enums.IconType;
import org.booklore.repository.ShelfRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserDefaultsServiceTest {
@Mock
private ShelfRepository shelfRepository;
@Mock
private ObjectMapper objectMapper;
@Mock
private DefaultUserSettingsProvider defaultSettingsProvider;
@InjectMocks
private UserDefaultsService userDefaultsService;
@Test
void addDefaultShelves_shouldCreateFavoritesShelfWithIcon() {
BookLoreUserEntity user = BookLoreUserEntity.builder()
.id(1L)
.username("testuser")
.build();
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
userDefaultsService.addDefaultShelves(user);
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
verify(shelfRepository).save(captor.capture());
ShelfEntity saved = captor.getValue();
assertEquals("Favorites", saved.getName());
assertEquals("heart", saved.getIcon());
assertEquals(IconType.PRIME_NG, saved.getIconType());
assertEquals(user, saved.getUser());
}
@Test
void addDefaultShelves_shouldSetExplicitIconType() {
BookLoreUserEntity user = BookLoreUserEntity.builder()
.id(2L)
.username("anotheruser")
.build();
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
userDefaultsService.addDefaultShelves(user);
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
verify(shelfRepository).save(captor.capture());
ShelfEntity saved = captor.getValue();
assertNotNull(saved.getIconType(), "Default shelf must have an explicit iconType since entity no longer has a default");
assertNotNull(saved.getIcon(), "Default shelf must have an explicit icon since entity no longer has a default");
}
}