Skip to content

Commit 11dea38

Browse files
authored
fix metadata search save clearing age rating and content rating (#3069) (#3098)
1 parent f604a69 commit 11dea38

File tree

5 files changed

+206
-9
lines changed

5 files changed

+206
-9
lines changed

booklore-api/src/main/java/org/booklore/controller/MetadataController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ public Flux<BookMetadata> getMetadataList(
6565
public ResponseEntity<BookMetadata> updateMetadata(
6666
@Parameter(description = "Metadata update wrapper") @RequestBody MetadataUpdateWrapper metadataUpdateWrapper,
6767
@Parameter(description = "ID of the book") @PathVariable long bookId,
68-
@Parameter(description = "Merge categories") @RequestParam(defaultValue = "false") boolean mergeCategories) {
68+
@Parameter(description = "Merge categories") @RequestParam(defaultValue = "false") boolean mergeCategories,
69+
@Parameter(description = "Replace mode") @RequestParam(defaultValue = "REPLACE_ALL") MetadataReplaceMode replaceMode) {
6970
BookEntity bookEntity = bookRepository.findAllWithMetadataByIds(java.util.Collections.singleton(bookId)).stream()
7071
.findFirst()
7172
.orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
@@ -75,7 +76,7 @@ public ResponseEntity<BookMetadata> updateMetadata(
7576
.metadataUpdateWrapper(metadataUpdateWrapper)
7677
.updateThumbnail(true)
7778
.mergeCategories(mergeCategories)
78-
.replaceMode(MetadataReplaceMode.REPLACE_ALL)
79+
.replaceMode(replaceMode)
7980
.mergeMoods(false)
8081
.mergeTags(false)
8182
.build();

booklore-api/src/test/java/org/booklore/controller/MetadataControllerTest.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.booklore.model.entity.BookMetadataEntity;
1010
import org.booklore.repository.BookRepository;
1111
import org.booklore.service.audit.AuditService;
12+
import org.booklore.model.enums.MetadataReplaceMode;
1213
import org.booklore.service.metadata.*;
1314
import org.junit.jupiter.api.Test;
1415
import org.junit.jupiter.api.extension.ExtendWith;
@@ -45,7 +46,7 @@ class MetadataControllerTest {
4546
@InjectMocks
4647
private MetadataController metadataController;
4748

48-
private MetadataUpdateContext captureContextFromUpdate() {
49+
private MetadataUpdateContext captureContextFromUpdate(MetadataReplaceMode replaceMode) {
4950
long bookId = 1L;
5051
MetadataUpdateWrapper wrapper = MetadataUpdateWrapper.builder().build();
5152
BookEntity bookEntity = new BookEntity();
@@ -55,7 +56,7 @@ private MetadataUpdateContext captureContextFromUpdate() {
5556
when(bookRepository.findAllWithMetadataByIds(java.util.Collections.singleton(bookId))).thenReturn(java.util.List.of(bookEntity));
5657
when(bookMetadataMapper.toBookMetadata(any(), anyBoolean())).thenReturn(new BookMetadata());
5758

58-
metadataController.updateMetadata(wrapper, bookId, true);
59+
metadataController.updateMetadata(wrapper, bookId, true, replaceMode);
5960

6061
ArgumentCaptor<MetadataUpdateContext> captor = ArgumentCaptor.forClass(MetadataUpdateContext.class);
6162
verify(bookMetadataUpdater).setBookMetadata(captor.capture());
@@ -64,9 +65,23 @@ private MetadataUpdateContext captureContextFromUpdate() {
6465

6566
@Test
6667
void updateMetadata_shouldDisableMergingForTagsAndMoods() {
67-
MetadataUpdateContext context = captureContextFromUpdate();
68+
MetadataUpdateContext context = captureContextFromUpdate(MetadataReplaceMode.REPLACE_ALL);
6869

6970
assertFalse(context.isMergeTags(), "mergeTags should be false to allow deletion of tags");
7071
assertFalse(context.isMergeMoods(), "mergeMoods should be false to allow deletion of moods");
7172
}
73+
74+
@Test
75+
void updateMetadata_shouldPassReplaceModeFromParam() {
76+
MetadataUpdateContext context = captureContextFromUpdate(MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
77+
78+
assertEquals(MetadataReplaceMode.REPLACE_WHEN_PROVIDED, context.getReplaceMode());
79+
}
80+
81+
@Test
82+
void updateMetadata_shouldDefaultToReplaceAll() {
83+
MetadataUpdateContext context = captureContextFromUpdate(MetadataReplaceMode.REPLACE_ALL);
84+
85+
assertEquals(MetadataReplaceMode.REPLACE_ALL, context.getReplaceMode());
86+
}
7287
}

booklore-api/src/test/java/org/booklore/service/metadata/BookMetadataUpdaterTest.java

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,4 +554,185 @@ void setBookMetadata_withReplaceMissingMode_shouldNotReplaceExistingTitle() {
554554
assertEquals("Existing Title", bookEntity.getMetadata().getTitle(),
555555
"Title should NOT be replaced when using REPLACE_MISSING mode and title exists");
556556
}
557+
558+
@Test
559+
void setBookMetadata_withReplaceWhenProvided_shouldPreserveExistingAgeRatingWhenIncomingIsNull() {
560+
BookEntity bookEntity = createBookEntity();
561+
bookEntity.getMetadata().setAgeRating(16);
562+
bookEntity.getMetadata().setAgeRatingLocked(false);
563+
564+
BookMetadata newMetadata = new BookMetadata();
565+
newMetadata.setTitle("Some Title");
566+
567+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
568+
569+
bookMetadataUpdater.setBookMetadata(context);
570+
571+
assertEquals(16, bookEntity.getMetadata().getAgeRating(),
572+
"ageRating should be preserved when incoming value is null and mode is REPLACE_WHEN_PROVIDED");
573+
}
574+
575+
@Test
576+
void setBookMetadata_withReplaceWhenProvided_shouldPreserveExistingContentRatingWhenIncomingIsNull() {
577+
BookEntity bookEntity = createBookEntity();
578+
bookEntity.getMetadata().setContentRating("Mature");
579+
bookEntity.getMetadata().setContentRatingLocked(false);
580+
581+
BookMetadata newMetadata = new BookMetadata();
582+
newMetadata.setTitle("Some Title");
583+
584+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
585+
586+
bookMetadataUpdater.setBookMetadata(context);
587+
588+
assertEquals("Mature", bookEntity.getMetadata().getContentRating(),
589+
"contentRating should be preserved when incoming value is null and mode is REPLACE_WHEN_PROVIDED");
590+
}
591+
592+
@Test
593+
void setBookMetadata_withReplaceAll_shouldClearAgeRatingWhenIncomingIsNull() {
594+
BookEntity bookEntity = createBookEntity();
595+
bookEntity.getMetadata().setAgeRating(16);
596+
bookEntity.getMetadata().setAgeRatingLocked(false);
597+
598+
BookMetadata newMetadata = new BookMetadata();
599+
600+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_ALL);
601+
602+
bookMetadataUpdater.setBookMetadata(context);
603+
604+
assertNull(bookEntity.getMetadata().getAgeRating(),
605+
"ageRating should be cleared when incoming value is null and mode is REPLACE_ALL");
606+
}
607+
608+
@Test
609+
void setBookMetadata_withReplaceAll_shouldClearContentRatingWhenIncomingIsNull() {
610+
BookEntity bookEntity = createBookEntity();
611+
bookEntity.getMetadata().setContentRating("Mature");
612+
bookEntity.getMetadata().setContentRatingLocked(false);
613+
614+
BookMetadata newMetadata = new BookMetadata();
615+
616+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_ALL);
617+
618+
bookMetadataUpdater.setBookMetadata(context);
619+
620+
assertNull(bookEntity.getMetadata().getContentRating(),
621+
"contentRating should be cleared when incoming value is null and mode is REPLACE_ALL");
622+
}
623+
624+
@Test
625+
void setBookMetadata_withReplaceWhenProvided_shouldUpdateAgeRatingWhenProvided() {
626+
BookEntity bookEntity = createBookEntity();
627+
bookEntity.getMetadata().setAgeRating(12);
628+
bookEntity.getMetadata().setAgeRatingLocked(false);
629+
630+
BookMetadata newMetadata = new BookMetadata();
631+
newMetadata.setAgeRating(18);
632+
633+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
634+
635+
bookMetadataUpdater.setBookMetadata(context);
636+
637+
assertEquals(18, bookEntity.getMetadata().getAgeRating(),
638+
"ageRating should be updated when a new value is provided");
639+
}
640+
641+
@Test
642+
void setBookMetadata_withReplaceWhenProvided_shouldUpdateContentRatingWhenProvided() {
643+
BookEntity bookEntity = createBookEntity();
644+
bookEntity.getMetadata().setContentRating("Teen");
645+
bookEntity.getMetadata().setContentRatingLocked(false);
646+
647+
BookMetadata newMetadata = new BookMetadata();
648+
newMetadata.setContentRating("Mature");
649+
650+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
651+
652+
bookMetadataUpdater.setBookMetadata(context);
653+
654+
assertEquals("Mature", bookEntity.getMetadata().getContentRating(),
655+
"contentRating should be updated when a new value is provided");
656+
}
657+
658+
@Test
659+
void setBookMetadata_withReplaceWhenProvided_shouldPreserveExistingTitleWhenIncomingIsNull() {
660+
BookEntity bookEntity = createBookEntity();
661+
bookEntity.getMetadata().setTitle("Existing Title");
662+
bookEntity.getMetadata().setTitleLocked(false);
663+
664+
BookMetadata newMetadata = new BookMetadata();
665+
666+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
667+
668+
bookMetadataUpdater.setBookMetadata(context);
669+
670+
assertEquals("Existing Title", bookEntity.getMetadata().getTitle(),
671+
"Title should be preserved when incoming value is null and mode is REPLACE_WHEN_PROVIDED");
672+
}
673+
674+
@Test
675+
void setBookMetadata_withReplaceWhenProvided_shouldPreserveAuthorsWhenIncomingIsEmpty() {
676+
BookEntity bookEntity = createBookEntity();
677+
Set<AuthorEntity> existingAuthors = new HashSet<>();
678+
existingAuthors.add(AuthorEntity.builder().name("Author1").build());
679+
bookEntity.getMetadata().setAuthors(existingAuthors);
680+
bookEntity.getMetadata().setAuthorsLocked(false);
681+
682+
BookMetadata newMetadata = new BookMetadata();
683+
684+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
685+
686+
bookMetadataUpdater.setBookMetadata(context);
687+
688+
assertEquals(1, bookEntity.getMetadata().getAuthors().size(),
689+
"Authors should be preserved when incoming is null/empty and mode is REPLACE_WHEN_PROVIDED");
690+
}
691+
692+
@Test
693+
void setBookMetadata_withReplaceWhenProvided_lockedFieldShouldNotBeUpdated() {
694+
BookEntity bookEntity = createBookEntity();
695+
bookEntity.getMetadata().setAgeRating(12);
696+
bookEntity.getMetadata().setAgeRatingLocked(true);
697+
698+
BookMetadata newMetadata = new BookMetadata();
699+
newMetadata.setAgeRating(18);
700+
701+
MetadataUpdateContext context = buildContext(bookEntity, newMetadata, MetadataReplaceMode.REPLACE_WHEN_PROVIDED);
702+
703+
bookMetadataUpdater.setBookMetadata(context);
704+
705+
assertEquals(12, bookEntity.getMetadata().getAgeRating(),
706+
"Locked ageRating should not be updated even when a new value is provided");
707+
}
708+
709+
private BookEntity createBookEntity() {
710+
BookEntity bookEntity = new BookEntity();
711+
bookEntity.setId(1L);
712+
BookMetadataEntity metadataEntity = new BookMetadataEntity();
713+
metadataEntity.setBook(bookEntity);
714+
bookEntity.setMetadata(metadataEntity);
715+
716+
BookFileEntity primaryFile = new BookFileEntity();
717+
primaryFile.setBook(bookEntity);
718+
primaryFile.setBookType(BookFileType.EPUB);
719+
primaryFile.setBookFormat(true);
720+
primaryFile.setFileSubPath("sub");
721+
primaryFile.setFileName("file.epub");
722+
bookEntity.setBookFiles(List.of(primaryFile));
723+
724+
return bookEntity;
725+
}
726+
727+
private MetadataUpdateContext buildContext(BookEntity bookEntity, BookMetadata newMetadata, MetadataReplaceMode replaceMode) {
728+
MetadataUpdateWrapper wrapper = MetadataUpdateWrapper.builder()
729+
.metadata(newMetadata)
730+
.build();
731+
732+
return MetadataUpdateContext.builder()
733+
.bookEntity(bookEntity)
734+
.metadataUpdateWrapper(wrapper)
735+
.replaceMode(replaceMode)
736+
.build();
737+
}
557738
}

booklore-ui/src/app/features/book/service/book-metadata-manage.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export class BookMetadataManageService {
2424
private bookService = inject(BookService);
2525
private readonly t = inject(TranslocoService);
2626

27-
updateBookMetadata(bookId: number | undefined, wrapper: MetadataUpdateWrapper, mergeCategories: boolean): Observable<BookMetadata> {
28-
const params = new HttpParams().set('mergeCategories', mergeCategories.toString());
27+
updateBookMetadata(bookId: number | undefined, wrapper: MetadataUpdateWrapper, mergeCategories: boolean, replaceMode: 'REPLACE_ALL' | 'REPLACE_WHEN_PROVIDED' = 'REPLACE_ALL'): Observable<BookMetadata> {
28+
const params = new HttpParams().set('mergeCategories', mergeCategories.toString()).set('replaceMode', replaceMode);
2929
return this.http.put<BookMetadata>(`${this.url}/${bookId}/metadata`, wrapper, {params}).pipe(
3030
map(updatedMetadata => {
3131
this.bookSocketService.handleBookMetadataUpdate(bookId!, updatedMetadata);

booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export class MetadataPickerComponent implements OnInit {
289289
const updatedBookMetadata = this.buildMetadataWrapper(undefined);
290290

291291
const requests: Observable<unknown>[] = [
292-
this.bookMetadataManageService.updateBookMetadata(this.currentBookId, updatedBookMetadata, false)
292+
this.bookMetadataManageService.updateBookMetadata(this.currentBookId, updatedBookMetadata, false, 'REPLACE_WHEN_PROVIDED')
293293
];
294294

295295
// Handle audiobook cover upload when fetched from Audible provider
@@ -517,7 +517,7 @@ export class MetadataPickerComponent implements OnInit {
517517
}
518518

519519
private updateMetadata(shouldLockAllFields: boolean | undefined): void {
520-
this.bookMetadataManageService.updateBookMetadata(this.currentBookId, this.buildMetadataWrapper(shouldLockAllFields), false).subscribe({
520+
this.bookMetadataManageService.updateBookMetadata(this.currentBookId, this.buildMetadataWrapper(shouldLockAllFields), false, 'REPLACE_WHEN_PROVIDED').subscribe({
521521
next: () => {
522522
if (shouldLockAllFields !== undefined) {
523523
this.messageService.add({

0 commit comments

Comments
 (0)