feat: Add-to-Plan flows C4/C5/C6 — recipe picker, quick actions, day picker #44

Merged
marcel merged 13 commits from feat/issue-42-add-to-plan into master 2026-04-09 09:53:08 +02:00
2 changed files with 68 additions and 0 deletions
Showing only changes of commit a52b0a9d24 - Show all commits

View File

@@ -1,5 +1,6 @@
package com.recipeapp.planning;
import com.recipeapp.common.RequiresHouseholdRole;
import com.recipeapp.planning.dto.*;
import com.recipeapp.recipe.HouseholdResolver;
import jakarta.validation.Valid;
@@ -40,6 +41,7 @@ public class WeekPlanController {
}
@PostMapping("/{id}/slots")
@RequiresHouseholdRole("planner")
public ResponseEntity<SlotResponse> addSlot(
Principal principal,
@PathVariable UUID id,
@@ -50,6 +52,7 @@ public class WeekPlanController {
}
@PatchMapping("/{planId}/slots/{slotId}")
@RequiresHouseholdRole("planner")
public SlotResponse updateSlot(
Principal principal,
@PathVariable UUID planId,
@@ -61,6 +64,7 @@ public class WeekPlanController {
@DeleteMapping("/{planId}/slots/{slotId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@RequiresHouseholdRole("planner")
public void deleteSlot(
Principal principal,
@PathVariable UUID planId,

View File

@@ -3,9 +3,11 @@ package com.recipeapp.planning;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.recipeapp.common.GlobalExceptionHandler;
import com.recipeapp.common.HouseholdRoleInterceptor;
import com.recipeapp.common.ValidationException;
import com.recipeapp.planning.dto.*;
import com.recipeapp.recipe.HouseholdResolver;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -13,6 +15,8 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@@ -49,6 +53,11 @@ class WeekPlanControllerTest {
.build();
}
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void getWeekPlanShouldReturn200() throws Exception {
var plan = new WeekPlanResponse(PLAN_ID, WEEK_START, "draft", null, List.of());
@@ -182,4 +191,59 @@ class WeekPlanControllerTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.score").value(7.5));
}
@Test
void addSlotShouldReturn403ForMemberRole() throws Exception {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("member@example.com", null));
when(householdResolver.resolveRole("member@example.com")).thenReturn("member");
MockMvc mockMvcWithInterceptor = MockMvcBuilders.standaloneSetup(weekPlanController)
.setControllerAdvice(new GlobalExceptionHandler())
.addInterceptors(new HouseholdRoleInterceptor(householdResolver))
.build();
var recipeId = UUID.randomUUID();
mockMvcWithInterceptor.perform(post("/v1/week-plans/{id}/slots", PLAN_ID)
.principal(() -> "member@example.com")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(
new CreateSlotRequest(WEEK_START.plusDays(1), recipeId))))
.andExpect(status().isForbidden());
}
@Test
void updateSlotShouldReturn403ForMemberRole() throws Exception {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("member@example.com", null));
when(householdResolver.resolveRole("member@example.com")).thenReturn("member");
MockMvc mockMvcWithInterceptor = MockMvcBuilders.standaloneSetup(weekPlanController)
.setControllerAdvice(new GlobalExceptionHandler())
.addInterceptors(new HouseholdRoleInterceptor(householdResolver))
.build();
var recipeId = UUID.randomUUID();
mockMvcWithInterceptor.perform(patch("/v1/week-plans/{planId}/slots/{slotId}", PLAN_ID, SLOT_ID)
.principal(() -> "member@example.com")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new UpdateSlotRequest(recipeId))))
.andExpect(status().isForbidden());
}
@Test
void deleteSlotShouldReturn403ForMemberRole() throws Exception {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("member@example.com", null));
when(householdResolver.resolveRole("member@example.com")).thenReturn("member");
MockMvc mockMvcWithInterceptor = MockMvcBuilders.standaloneSetup(weekPlanController)
.setControllerAdvice(new GlobalExceptionHandler())
.addInterceptors(new HouseholdRoleInterceptor(householdResolver))
.build();
mockMvcWithInterceptor.perform(delete("/v1/week-plans/{planId}/slots/{slotId}", PLAN_ID, SLOT_ID)
.principal(() -> "member@example.com"))
.andExpect(status().isForbidden());
}
}