From bb2a4d70b200a021f4adfa62fc836fdee3dad24f Mon Sep 17 00:00:00 2001 From: Cedric Hornberger Date: Mon, 4 May 2026 22:46:29 +0200 Subject: [PATCH] feat: add ExpenseListController validation and authorization tests --- .../controller/ExpenseListController.java | 2 +- .../controller/ExpenseListControllerTest.java | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/test/java/de/zendric/app/xpensely_Server/controller/ExpenseListControllerTest.java diff --git a/src/main/java/de/zendric/app/xpensely_server/controller/ExpenseListController.java b/src/main/java/de/zendric/app/xpensely_server/controller/ExpenseListController.java index cb5fb13..7d86cf7 100644 --- a/src/main/java/de/zendric/app/xpensely_server/controller/ExpenseListController.java +++ b/src/main/java/de/zendric/app/xpensely_server/controller/ExpenseListController.java @@ -19,7 +19,7 @@ import de.zendric.app.xpensely_server.services.UserService; @RestController @RequestMapping("/api/expenselist") -class ExpenseListController { +public class ExpenseListController { private final ExpenseListService expenseListService; private final UserService userService; diff --git a/src/test/java/de/zendric/app/xpensely_Server/controller/ExpenseListControllerTest.java b/src/test/java/de/zendric/app/xpensely_Server/controller/ExpenseListControllerTest.java new file mode 100644 index 0000000..2cba572 --- /dev/null +++ b/src/test/java/de/zendric/app/xpensely_Server/controller/ExpenseListControllerTest.java @@ -0,0 +1,135 @@ +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.ExpenseList; +import de.zendric.app.xpensely_server.security.AuthenticatedUserResolver; +import de.zendric.app.xpensely_server.services.CategoryService; +import de.zendric.app.xpensely_server.services.ExpenseListService; +import de.zendric.app.xpensely_server.services.UserService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ExpenseListController.class) +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +class ExpenseListControllerTest { + + @Autowired MockMvc mockMvc; + @MockitoBean ExpenseListService expenseListService; + @MockitoBean UserService userService; + @MockitoBean CategoryService categoryService; + @MockitoBean AuthenticatedUserResolver authenticatedUserResolver; + + // --- Validation tests --- + + @Test + void addExpense_blankTitle_returns400() throws Exception { + mockMvc.perform(post("/api/expenselist/1/add") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"title\":\"\",\"owner\":\"alice\",\"amount\":10.0,\"date\":\"2026-05-04\",\"category\":\"Food\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.title").exists()); + } + + @Test + void addExpense_negativeAmount_returns400() throws Exception { + mockMvc.perform(post("/api/expenselist/1/add") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"title\":\"Lunch\",\"owner\":\"alice\",\"amount\":-5.0,\"date\":\"2026-05-04\",\"category\":\"Food\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.amount").exists()); + } + + @Test + void addExpense_nullDate_returns400() throws Exception { + mockMvc.perform(post("/api/expenselist/1/add") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"title\":\"Lunch\",\"owner\":\"alice\",\"amount\":10.0,\"category\":\"Food\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.date").exists()); + } + + @Test + void acceptInvite_blankCode_returns400() throws Exception { + mockMvc.perform(post("/api/expenselist/accept-invite") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"inviteCode\":\"\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.inviteCode").exists()); + } + + @Test + void acceptInvite_wrongCodeLength_returns400() throws Exception { + mockMvc.perform(post("/api/expenselist/accept-invite") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"inviteCode\":\"ABC\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.inviteCode").exists()); + } + + // --- Authorization tests --- + + @Test + void getById_authenticatedUserNotMember_returns403() throws Exception { + AppUser owner = new AppUser(); owner.setId(1L); + AppUser requester = new AppUser(); requester.setId(2L); + ExpenseList list = new ExpenseList(); list.setId(1L); list.setOwner(owner); + + when(expenseListService.findById(1L)).thenReturn(Optional.of(list)); + when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(requester); + + mockMvc.perform(get("/api/expenselist/byId").param("id", "1")) + .andExpect(status().isForbidden()); + } + + @Test + void getById_authenticatedUserIsOwner_returns200() throws Exception { + AppUser owner = new AppUser(); owner.setId(1L); + ExpenseList list = new ExpenseList(); list.setId(1L); list.setOwner(owner); + + when(expenseListService.findById(1L)).thenReturn(Optional.of(list)); + when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(owner); + + mockMvc.perform(get("/api/expenselist/byId").param("id", "1")) + .andExpect(status().isOk()); + } + + @Test + void deleteList_nonOwner_returns403() throws Exception { + AppUser owner = new AppUser(); owner.setId(1L); + AppUser nonOwner = new AppUser(); nonOwner.setId(2L); + ExpenseList list = new ExpenseList(); list.setId(5L); list.setOwner(owner); + + when(expenseListService.findById(5L)).thenReturn(Optional.of(list)); + when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(nonOwner); + + mockMvc.perform(delete("/api/expenselist/5")) + .andExpect(status().isForbidden()); + } + + @Test + void getMine_returnsCurrentUserLists() throws Exception { + AppUser user = new AppUser(); user.setId(3L); + + when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(user); + when(expenseListService.findByUserId(3L)).thenReturn(List.of(new ExpenseList())); + + mockMvc.perform(get("/api/expenselist/mine")) + .andExpect(status().isOk()); + } +}