Write more recipe drafts integration tests.

This commit is contained in:
Jesse Brault 2026-01-25 17:35:21 -06:00
parent f43751ab4f
commit c088cb2253
3 changed files with 152 additions and 13 deletions

View File

@ -9,6 +9,7 @@ import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException; import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService; import app.mealsmadeeasy.api.user.UserService;
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -26,6 +27,7 @@ import java.util.UUID;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThrows;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -82,11 +84,10 @@ public class RecipeDraftsControllerIntegrationTests {
} }
@Test @Test
public void whenNoUser_getReturnsUnauthorized() throws Exception { public void whenNoDraftAndNoPrincipal_getReturnsUnauthorized() throws Exception {
this.mockMvc.perform( this.mockMvc.perform(
get("/recipe-drafts/{fakeId}", UUID.randomUUID().toString()) get("/recipe-drafts/{fakeId}", UUID.randomUUID().toString())
) ).andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized());
} }
@Test @Test
@ -111,6 +112,26 @@ public class RecipeDraftsControllerIntegrationTests {
.andExpect(jsonPath("$.mainImage", is(nullValue()))); .andExpect(jsonPath("$.mainImage", is(nullValue())));
} }
@Test
public void whenDraftExistsButNoPrincipal_returnsUnauthorized() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
this.mockMvc.perform(
get("/recipe-drafts/{id}", recipeDraft.getId())
).andExpect(status().isUnauthorized());
}
@Test
public void whenDraftExistsButWrongPrincipal_returnsForbidden() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
final User wrongViewer = this.seedUser();
this.mockMvc.perform(
get("/recipe-drafts/{id}", recipeDraft.getId())
.header("Authorization", "Bearer " + this.getAccessToken(wrongViewer))
).andExpect(status().isForbidden());
}
@Test @Test
public void whenDraftsExist_returnDrafts() throws Exception { public void whenDraftsExist_returnDrafts() throws Exception {
final User owner = this.seedUser(); final User owner = this.seedUser();
@ -125,6 +146,21 @@ public class RecipeDraftsControllerIntegrationTests {
.andExpect(jsonPath("$", hasSize(2))); .andExpect(jsonPath("$", hasSize(2)));
} }
@Test
public void whenDraftsExistButWrongPrincipal_returnsNoDrafts() throws Exception {
final User owner = this.seedUser();
this.recipeService.createDraft(owner); // seed 1
this.recipeService.createDraft(owner); // seed 2
final User wrongViewer = this.seedUser();
this.mockMvc.perform(
get("/recipe-drafts")
.header("Authorization", "Bearer " + this.getAccessToken(wrongViewer))
)
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$", hasSize(0)));
}
@Test @Test
public void manualDraft_returnsDraft() throws Exception { public void manualDraft_returnsDraft() throws Exception {
final User owner = this.seedUser(); final User owner = this.seedUser();
@ -165,16 +201,14 @@ public class RecipeDraftsControllerIntegrationTests {
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andExpect(jsonPath("$.created", is(notNullValue()))) .andExpect(jsonPath("$.created", is(notNullValue())))
.andExpect(jsonPath("$.state", is(RecipeDraft.State.INFER.toString()))) .andExpect(jsonPath("$.state", is(RecipeDraft.State.INFER.toString())))
.andExpect(jsonPath("$.owner.id", is(owner.getId()))); .andExpect(jsonPath("$.owner.id", is(owner.getId())))
.andExpect(jsonPath("$.owner.username", is(owner.getUsername())));
} }
} }
@Test private static RecipeDraftUpdateBody getTestRecipeDraftUpdateBody() {
public void whenUpdate_returnsUpdated() throws Exception { return RecipeDraftUpdateBody.builder()
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
final RecipeDraftUpdateBody updateBody = RecipeDraftUpdateBody.builder()
.title("Test Title") .title("Test Title")
.slug("test-slug") .slug("test-slug")
.preparationTime(15) .preparationTime(15)
@ -189,6 +223,13 @@ public class RecipeDraftsControllerIntegrationTests {
.build() .build()
)) ))
.build(); .build();
}
@Test
public void whenUpdate_returnsUpdated() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
final RecipeDraftUpdateBody updateBody = getTestRecipeDraftUpdateBody();
this.mockMvc.perform( this.mockMvc.perform(
put("/recipe-drafts/{id}", recipeDraft.getId()) put("/recipe-drafts/{id}", recipeDraft.getId())
.header("Authorization", "Bearer " + this.getAccessToken(owner)) .header("Authorization", "Bearer " + this.getAccessToken(owner))
@ -208,4 +249,102 @@ public class RecipeDraftsControllerIntegrationTests {
.andExpect(jsonPath("$.ingredients[0].notes", is("Separated"))); .andExpect(jsonPath("$.ingredients[0].notes", is("Separated")));
} }
@Test
public void whenUpdateNoPrincipal_returnsUnauthorized() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
final RecipeDraftUpdateBody updateBody = getTestRecipeDraftUpdateBody();
this.mockMvc.perform(
put("/recipe-drafts/{id}", recipeDraft.getId())
.header("Content-Type", "application/json")
.content(this.objectMapper.writeValueAsString(updateBody))
).andExpect(status().isUnauthorized());
}
@Test
public void whenWrongPrincipalUpdate_returnsForbidden() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
final RecipeDraftUpdateBody updateBody = getTestRecipeDraftUpdateBody();
final User wrongModifier = this.seedUser();
this.mockMvc.perform(
put("/recipe-drafts/{id}", recipeDraft.getId())
.header("Authorization", "Bearer " + this.getAccessToken(wrongModifier))
.header("Content-Type", "application/json")
.content(this.objectMapper.writeValueAsString(updateBody))
).andExpect(status().isForbidden());
}
@Test
public void whenDelete_draftIsDeleted() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
this.mockMvc.perform(
delete("/recipe-drafts/{id}", recipeDraft.getId())
.header("Authorization", "Bearer " + this.getAccessToken(owner))
)
.andExpect(status().isNoContent());
assertThrows(NoSuchEntityWithIdException.class, () -> this.recipeService.getDraftById(recipeDraft.getId()));
}
@Test
public void whenDeleteWithNoPrincipal_returnsUnauthorized() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
this.mockMvc.perform(
delete("/recipe-drafts/{id}", recipeDraft.getId())
).andExpect(status().isUnauthorized());
}
@Test
public void whenPublish_expectDraftDeletedAndRecipeCreated() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
recipeDraft.setTitle("Test Title");
recipeDraft.setSlug("test-slug");
recipeDraft.setRawText("# Hello, World!");
this.recipeService.saveDraft(recipeDraft);
this.mockMvc.perform(
post("/recipe-drafts/{id}/publish", recipeDraft.getId())
.header("Authorization", "Bearer " + this.getAccessToken(owner))
)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is(notNullValue())))
.andExpect(jsonPath("$.created", is(notNullValue())))
.andExpect(jsonPath("$.modified", is(nullValue())))
.andExpect(jsonPath("$.title", is("Test Title")))
.andExpect(jsonPath("$.slug", is("test-slug")))
.andExpect(jsonPath("$.preparationTime", is(nullValue())))
.andExpect(jsonPath("$.cookingTime", is(nullValue())))
.andExpect(jsonPath("$.totalTime", is(nullValue())))
.andExpect(jsonPath("$.text", is("<h1>Hello, World!</h1>")))
.andExpect(jsonPath("$.owner.id", is(owner.getId())))
.andExpect(jsonPath("$.owner.username", is(owner.getUsername())))
.andExpect(jsonPath("$.starCount", is(0)))
.andExpect(jsonPath("$.viewerCount", is(0)))
.andExpect(jsonPath("$.mainImage", is(nullValue())))
.andExpect(jsonPath("$.public", is(false)));
assertThrows(NoSuchEntityWithIdException.class, () -> this.recipeService.getDraftById(recipeDraft.getId()));
}
@Test
public void whenPublishWithNoPrincipal_returnsUnauthorized() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
this.mockMvc.perform(
post("/recipe-drafts/{id}/publish", recipeDraft.getId())
).andExpect(status().isUnauthorized());
}
@Test
public void whenPublishWrongPrincipal_returnsForbidden() throws Exception {
final User owner = this.seedUser();
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
final User wrongPublisher = this.seedUser();
this.mockMvc.perform(
post("/recipe-drafts/{id}/publish", recipeDraft.getId())
.header("Authorization", "Bearer " + this.getAccessToken(wrongPublisher))
).andExpect(status().isForbidden());
}
} }

View File

@ -92,7 +92,7 @@ public class RecipeDraftsController {
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@PostMapping("/{id}") @PostMapping("/{id}/publish")
public ResponseEntity<FullRecipeView> publishRecipeDraft( public ResponseEntity<FullRecipeView> publishRecipeDraft(
@AuthenticationPrincipal User modifier, @AuthenticationPrincipal User modifier,
@PathVariable UUID id @PathVariable UUID id

View File

@ -244,9 +244,9 @@ public class RecipeService {
} }
public RecipeDraft getDraftById(UUID id) { public RecipeDraft getDraftById(UUID id) {
return this.recipeDraftRepository.findById(id).orElseThrow(() -> new RuntimeException( return this.recipeDraftRepository.findById(id).orElseThrow(
"RecipeDraft with id " + id + " not found" () -> new NoSuchEntityWithIdException(RecipeDraft.class, id)
)); );
} }
@PostAuthorize("@recipeDraftSecurity.isViewableBy(returnObject, #viewer)") @PostAuthorize("@recipeDraftSecurity.isViewableBy(returnObject, #viewer)")