chore(library)!: drop the full-replace PUT of a backlog entry

The Frontend sends PATCH since item 27 batch 2.
This commit is contained in:
lobo committed 2026-09-30 15:00:47 +02:00
1 parent 34d64e7215
commit 23bf5c41b4
5 files changed
+6 -100

No files matched your search

@@ -3,7 +3,6 @@ package core_service.modules.library.api;
import core_service.shared.api.ErrorResponse;
import core_service.modules.library.api.dto.AchievementSummaryDto;
import core_service.modules.library.api.dto.ActivityDto;
import core_service.modules.library.api.dto.BacklogEntryDto;
import core_service.modules.library.api.dto.BacklogPatch;
import core_service.modules.library.api.dto.BrokenImageReport;
import core_service.modules.library.api.dto.CorrectionDto;
@@ -219,16 +218,6 @@ public class LibraryController {
return ResponseEntity.badRequest().body(new ErrorResponse("INVALID_CORRECTION", message));
}
@PutMapping("/games/{appid}/backlog")
public ResponseEntity<?> updateBacklog(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable("appid") Long appid,
@Valid @RequestBody BacklogEntryDto entry) {
return backlogService.update(principal.getIdAsLong(), appid, entry)
.<ResponseEntity<?>>map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(GAME_NOT_FOUND));
}
@PatchMapping("/games/{appid}/backlog")
public ResponseEntity<?> patchBacklog(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable("appid") Long appid,
@@ -24,23 +24,6 @@ public class BacklogService {
private final UserGameRepository userGameRepository;
/**
* @return empty when the game is not in the user's active library
*/
@Transactional
public Optional<BacklogEntryDto> update(Long userId, Long appid, BacklogEntryDto entry) {
return userGameRepository.findById(new UserGameId(userId, appid))
.filter(game -> game.getRemovedAt() == null)
.map(game -> {
game.setStatus(entry.status());
game.setStatusSetByUser(true);
game.setRating(entry.rating());
game.setNotes(entry.notes() == null || entry.notes().isBlank() ? null : entry.notes());
game.setPriority(entry.priority());
return toDto(game);
});
}
/**
* Changes only the fields the patch sends. Only a sent status counts as set by the user, so a save of the notes
* alone still lets the sync move the game from BACKLOG to PLAYING.
@@ -60,88 +60,23 @@ class LibraryControllerBacklogTest extends AbstractIntegrationTest {
}
@Test
void updateBacklog_StoresAllFields() throws Exception {
mockMvc.perform(put("/api/library/games/220/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test").contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"FINISHED\",\"rating\":4,\"notes\":\"Great ending\",\"priority\":\"HIGH\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("FINISHED"))
.andExpect(jsonPath("$.rating").value(4));
UserGame game = stored();
assertEquals(GameStatus.FINISHED, game.getStatus());
assertEquals(4, game.getRating());
assertEquals("Great ending", game.getNotes());
assertEquals("HIGH", game.getPriority().name());
}
@Test
void updateBacklog_NullsClearOptionalFieldsAndBlankNotesAreNull() throws Exception {
UserGame game = stored();
game.setRating(5);
game.setNotes("old");
userGameRepository.save(game);
mockMvc.perform(put("/api/library/games/220/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test").contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"DROPPED\",\"notes\":\" \"}"))
.andExpect(status().isOk());
game = stored();
assertEquals(GameStatus.DROPPED, game.getStatus());
assertNull(game.getRating());
assertNull(game.getNotes());
assertNull(game.getPriority());
}
@ParameterizedTest
@ValueSource(strings = {
"{}",
"{\"status\":\"WISHLIST\"}",
"{\"status\":\"PLAYING\",\"rating\":0}",
"{\"status\":\"PLAYING\",\"rating\":6}",
"{\"status\":\"PLAYING\",\"priority\":\"URGENT\"}"})
void updateBacklog_InvalidBody_Is400AndStoresNothing(String body) throws Exception {
mockMvc.perform(put("/api/library/games/220/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test").contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isBadRequest());
assertEquals(GameStatus.BACKLOG, stored().getStatus());
}
@Test
void updateBacklog_FractionalRating_Is400() throws Exception {
mockMvc.perform(put("/api/library/games/220/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test").contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"PLAYING\",\"rating\":4.7}"))
.andExpect(status().isBadRequest());
void patchBacklog_FractionalRating_Is400() throws Exception {
patchBacklog(APPID, "{\"rating\":4.7}").andExpect(status().isBadRequest());
assertNull(stored().getRating());
}
@Test
void updateBacklog_NotesTooLong_Is400() throws Exception {
String notes = "x".repeat(2001);
mockMvc.perform(put("/api/library/games/220/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test").contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"PLAYING\",\"notes\":\"" + notes + "\"}"))
.andExpect(status().isBadRequest());
}
@Test
void updateBacklog_RemovedGame_Is404() throws Exception {
void patchBacklog_RemovedGame_Is404() throws Exception {
UserGame game = stored();
game.setRemovedAt(Instant.now());
userGameRepository.save(game);
mockMvc.perform(put("/api/library/games/220/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test").contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"PLAYING\"}"))
patchBacklog(APPID, "{\"notes\":\"x\"}")
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("GAME_NOT_FOUND"));
}
@Test
void updateBacklog_GameNotOwned_Is404() throws Exception {
mockMvc.perform(put("/api/library/games/999/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test").contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"PLAYING\"}"))
.andExpect(status().isNotFound());
}
private ResultActions patchBacklog(long appid, String body) throws Exception {
return mockMvc.perform(patch("/api/library/games/" + appid + "/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test")
.contentType(MediaType.APPLICATION_JSON).content(body));
@@ -24,7 +24,7 @@ import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -189,7 +189,7 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
// The dev filter falls back to the dummy user 1 for a token it rejects; user 7's game would answer 200.
@Test
void profileTicket_IsNoLoginForTheLibraryApi() throws Exception {
mockMvc.perform(put("/api/library/games/220/backlog").header("Authorization", "Bearer " + ticket(USER_ID))
mockMvc.perform(patch("/api/library/games/220/backlog").header("Authorization", "Bearer " + ticket(USER_ID))
.contentType("application/json").content("{\"status\":\"DROPPED\"}"))
.andExpect(status().isNotFound());
}