Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af648bca70 | |||
| b7e0fa1c8b |
@@ -15,6 +15,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import de.zendric.app.xpensely_server.model.*;
|
||||
import de.zendric.app.xpensely_server.model.DTO.HistoryPageDto;
|
||||
import de.zendric.app.xpensely_server.model.DTO.RecentHistoryEntryDto;
|
||||
import de.zendric.app.xpensely_server.security.AuthenticatedUserResolver;
|
||||
import de.zendric.app.xpensely_server.services.CategoryService;
|
||||
import de.zendric.app.xpensely_server.services.ExpenseListService;
|
||||
@@ -194,6 +195,14 @@ public class ExpenseListController {
|
||||
return ResponseEntity.ok(historyService.getHistory(id, page, size));
|
||||
}
|
||||
|
||||
@GetMapping("/history/recent")
|
||||
public ResponseEntity<List<RecentHistoryEntryDto>> getRecentHistory(
|
||||
@RequestParam(defaultValue = "4") int limit,
|
||||
Authentication authentication) {
|
||||
AppUser user = authenticatedUserResolver.resolveCurrentUser(authentication);
|
||||
return ResponseEntity.ok(historyService.getRecentForUser(user, limit));
|
||||
}
|
||||
|
||||
private void assertOwner(AppUser authenticated, ExpenseList list) {
|
||||
if (!list.getOwner().getId().equals(authenticated.getId()))
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.zendric.app.xpensely_server.model.DTO;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record RecentHistoryEntryDto(
|
||||
String type,
|
||||
String actorUsername,
|
||||
String expenseTitle,
|
||||
LocalDateTime timestamp,
|
||||
Long listId,
|
||||
String listName) {
|
||||
}
|
||||
@@ -13,6 +13,9 @@ public interface ExpenseHistoryRepository extends JpaRepository<ExpenseHistoryEn
|
||||
|
||||
Page<ExpenseHistoryEntry> findByExpenseListIdOrderByTimestampDescIdDesc(Long expenseListId, Pageable pageable);
|
||||
|
||||
Page<ExpenseHistoryEntry> findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc(
|
||||
Long ownerId, Long sharedWithId, Pageable pageable);
|
||||
|
||||
@Query("select h.expenseId from ExpenseHistoryEntry h")
|
||||
List<Long> findAllExpenseIds();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import tools.jackson.databind.ObjectMapper;
|
||||
import de.zendric.app.xpensely_server.model.AppUser;
|
||||
import de.zendric.app.xpensely_server.model.DTO.HistoryEntryDto;
|
||||
import de.zendric.app.xpensely_server.model.DTO.HistoryPageDto;
|
||||
import de.zendric.app.xpensely_server.model.DTO.RecentHistoryEntryDto;
|
||||
import de.zendric.app.xpensely_server.model.Expense;
|
||||
import de.zendric.app.xpensely_server.model.ExpenseHistoryEntry;
|
||||
import de.zendric.app.xpensely_server.model.ExpenseList;
|
||||
@@ -104,6 +105,23 @@ public class HistoryService {
|
||||
return new HistoryPageDto(entries, result.hasNext());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<RecentHistoryEntryDto> getRecentForUser(AppUser user, int limit) {
|
||||
int cappedLimit = Math.min(Math.max(limit, 1), 20);
|
||||
Page<ExpenseHistoryEntry> result = historyRepository
|
||||
.findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc(
|
||||
user.getId(), user.getId(), PageRequest.of(0, cappedLimit));
|
||||
return result.getContent().stream()
|
||||
.map(e -> new RecentHistoryEntryDto(
|
||||
e.getType().name(),
|
||||
e.getActor() == null ? null : e.getActor().getUsername(),
|
||||
e.getExpenseTitle(),
|
||||
e.getTimestamp(),
|
||||
e.getExpenseList().getId(),
|
||||
e.getExpenseList().getName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private HistoryEntryDto toDto(ExpenseHistoryEntry entry) {
|
||||
List<FieldChange> changes = List.of();
|
||||
if (entry.getChanges() != null) {
|
||||
|
||||
+46
@@ -3,6 +3,7 @@ package de.zendric.app.xpensely_Server.controller;
|
||||
import de.zendric.app.xpensely_server.controller.ExpenseListController;
|
||||
import de.zendric.app.xpensely_server.model.AppUser;
|
||||
import de.zendric.app.xpensely_server.model.DTO.HistoryPageDto;
|
||||
import de.zendric.app.xpensely_server.model.DTO.RecentHistoryEntryDto;
|
||||
import de.zendric.app.xpensely_server.model.Expense;
|
||||
import de.zendric.app.xpensely_server.model.ExpenseList;
|
||||
import de.zendric.app.xpensely_server.security.AuthenticatedUserResolver;
|
||||
@@ -370,4 +371,49 @@ class ExpenseListControllerTest {
|
||||
mockMvc.perform(get("/api/expenselist/99/history"))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
// --- Recent history endpoint ---
|
||||
|
||||
@Test
|
||||
void getRecentHistory_returnsArrayWithListInfo() throws Exception {
|
||||
AppUser user = new AppUser(); user.setId(1L); user.setUsername("alice");
|
||||
when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(user);
|
||||
when(historyService.getRecentForUser(user, 4)).thenReturn(List.of(
|
||||
new RecentHistoryEntryDto("UPDATED", "alice", "Groceries",
|
||||
LocalDateTime.of(2026, 7, 6, 12, 0), 10L, "Trip to Rome")));
|
||||
|
||||
mockMvc.perform(get("/api/expenselist/history/recent"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].type").value("UPDATED"))
|
||||
.andExpect(jsonPath("$[0].actorUsername").value("alice"))
|
||||
.andExpect(jsonPath("$[0].expenseTitle").value("Groceries"))
|
||||
.andExpect(jsonPath("$[0].listId").value(10))
|
||||
.andExpect(jsonPath("$[0].listName").value("Trip to Rome"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRecentHistory_passesLimitParam() throws Exception {
|
||||
AppUser user = new AppUser(); user.setId(1L);
|
||||
when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(user);
|
||||
when(historyService.getRecentForUser(user, 10)).thenReturn(List.of());
|
||||
|
||||
mockMvc.perform(get("/api/expenselist/history/recent").param("limit", "10"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRecentHistory_doesNotClashWithPerListHistoryRoute() throws Exception {
|
||||
// /api/expenselist/1/history must still hit the per-list handler.
|
||||
AppUser owner = new AppUser(); owner.setId(1L); owner.setUsername("alice");
|
||||
ExpenseList list = new ExpenseList(); list.setId(1L); list.setOwner(owner);
|
||||
when(expenseListService.findById(1L)).thenReturn(Optional.of(list));
|
||||
when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(owner);
|
||||
when(historyService.getHistory(1L, 0, 30))
|
||||
.thenReturn(new HistoryPageDto(List.of(), false));
|
||||
|
||||
mockMvc.perform(get("/api/expenselist/1/history"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.entries").isArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import de.zendric.app.xpensely_server.model.AppUser;
|
||||
import de.zendric.app.xpensely_server.model.DTO.HistoryPageDto;
|
||||
import de.zendric.app.xpensely_server.model.DTO.RecentHistoryEntryDto;
|
||||
import de.zendric.app.xpensely_server.model.Expense;
|
||||
import de.zendric.app.xpensely_server.model.ExpenseHistoryEntry;
|
||||
import de.zendric.app.xpensely_server.model.ExpenseList;
|
||||
@@ -206,4 +207,75 @@ class HistoryServiceTest {
|
||||
|
||||
assertThat(page.entries().get(0).changes()).isEmpty();
|
||||
}
|
||||
|
||||
private ExpenseHistoryEntry historyEntry(Long id, ExpenseList list, AppUser actor,
|
||||
HistoryEntryType type, String title, java.time.LocalDateTime timestamp) {
|
||||
ExpenseHistoryEntry e = new ExpenseHistoryEntry();
|
||||
e.setId(id);
|
||||
e.setExpenseList(list);
|
||||
e.setActor(actor);
|
||||
e.setType(type);
|
||||
e.setExpenseTitle(title);
|
||||
e.setTimestamp(timestamp);
|
||||
return e;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRecentForUser_mapsEntriesWithListIdAndName() {
|
||||
AppUser alice = user(1L, "alice");
|
||||
ExpenseList list = new ExpenseList();
|
||||
list.setId(10L);
|
||||
list.setName("Trip to Rome");
|
||||
ExpenseHistoryEntry entry = historyEntry(100L, list, alice,
|
||||
HistoryEntryType.UPDATED, "Groceries", java.time.LocalDateTime.of(2026, 7, 6, 12, 0));
|
||||
|
||||
when(historyRepository
|
||||
.findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc(
|
||||
eq(1L), eq(1L), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(entry)));
|
||||
|
||||
List<RecentHistoryEntryDto> result = service.getRecentForUser(alice, 4);
|
||||
|
||||
assertThat(result).containsExactly(new RecentHistoryEntryDto(
|
||||
"UPDATED", "alice", "Groceries",
|
||||
java.time.LocalDateTime.of(2026, 7, 6, 12, 0), 10L, "Trip to Rome"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRecentForUser_nullActor_mapsToNullUsername() {
|
||||
AppUser alice = user(1L, "alice");
|
||||
ExpenseList list = new ExpenseList();
|
||||
list.setId(10L);
|
||||
list.setName("Trip to Rome");
|
||||
ExpenseHistoryEntry entry = historyEntry(100L, list, null,
|
||||
HistoryEntryType.CREATED, "Taxi", java.time.LocalDateTime.of(2026, 7, 6, 12, 0));
|
||||
|
||||
when(historyRepository
|
||||
.findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc(
|
||||
eq(1L), eq(1L), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(entry)));
|
||||
|
||||
List<RecentHistoryEntryDto> result = service.getRecentForUser(alice, 4);
|
||||
|
||||
assertThat(result.get(0).actorUsername()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRecentForUser_clampsLimitBetween1And20() {
|
||||
AppUser alice = user(1L, "alice");
|
||||
when(historyRepository
|
||||
.findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc(
|
||||
eq(1L), eq(1L), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
|
||||
service.getRecentForUser(alice, 500);
|
||||
verify(historyRepository)
|
||||
.findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc(
|
||||
1L, 1L, PageRequest.of(0, 20));
|
||||
|
||||
service.getRecentForUser(alice, 0);
|
||||
verify(historyRepository)
|
||||
.findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc(
|
||||
1L, 1L, PageRequest.of(0, 1));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user