Add some tests, more to do.
This commit is contained in:
parent
2d2fa524fa
commit
db9e9eca07
@ -0,0 +1,117 @@
|
|||||||
|
package app.mealsmadeeasy.api.recipe;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.IntegrationTestsExtension;
|
||||||
|
import app.mealsmadeeasy.api.auth.AuthService;
|
||||||
|
import app.mealsmadeeasy.api.auth.LoginException;
|
||||||
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.user.UserCreateException;
|
||||||
|
import app.mealsmadeeasy.api.user.UserService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ExtendWith(IntegrationTestsExtension.class)
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
public class RecipeDraftsControllerIntegrationTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthService authService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecipeService recipeService;
|
||||||
|
|
||||||
|
private static final String TEST_PASSWORD = "test";
|
||||||
|
|
||||||
|
private User seedUser() {
|
||||||
|
final String uuid = UUID.randomUUID().toString();
|
||||||
|
try {
|
||||||
|
return this.userService.createUser(uuid, uuid + "@test.com", TEST_PASSWORD);
|
||||||
|
} catch (UserCreateException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAccessToken(User user) throws LoginException {
|
||||||
|
return this.authService.login(user.getUsername(), TEST_PASSWORD)
|
||||||
|
.getAccessToken()
|
||||||
|
.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenNoDraft_getReturnsNotFound() throws Exception {
|
||||||
|
final User user = this.seedUser();
|
||||||
|
final String fakeId = UUID.randomUUID().toString();
|
||||||
|
this.mockMvc.perform(
|
||||||
|
get("/recipe-drafts/{fakeId}", fakeId)
|
||||||
|
.header("Authorization", "Bearer " + this.getAccessToken(user))
|
||||||
|
)
|
||||||
|
.andExpect(status().isNotFound())
|
||||||
|
.andExpect(jsonPath("$.entityName", is("RecipeDraft")))
|
||||||
|
.andExpect(jsonPath("$.id", is(fakeId)))
|
||||||
|
.andExpect(jsonPath("$.message", is(notNullValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenNoUser_getReturnsUnauthorized() throws Exception {
|
||||||
|
this.mockMvc.perform(
|
||||||
|
get("/recipe-drafts/{fakeId}", UUID.randomUUID().toString())
|
||||||
|
)
|
||||||
|
.andExpect(status().isUnauthorized())
|
||||||
|
.andExpect(jsonPath("$.message", is(notNullValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenDraftExists_returnsDraftById() throws Exception {
|
||||||
|
final User owner = this.seedUser();
|
||||||
|
final RecipeDraft seeded = this.recipeService.createDraft(owner);
|
||||||
|
this.mockMvc.perform(
|
||||||
|
get("/recipe-drafts/{id}", seeded.getId())
|
||||||
|
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.id", is(seeded.getId().toString())))
|
||||||
|
.andExpect(jsonPath("$.created", is(notNullValue())))
|
||||||
|
.andExpect(jsonPath("$.modified", is(nullValue())))
|
||||||
|
.andExpect(jsonPath("$.state", is(RecipeDraft.State.ENTER_DATA.toString())))
|
||||||
|
.andExpect(jsonPath("$.title", is(nullValue())))
|
||||||
|
.andExpect(jsonPath("$.preparationTime", is(nullValue())))
|
||||||
|
.andExpect(jsonPath("$.cookingTime", is(nullValue())))
|
||||||
|
.andExpect(jsonPath("$.ingredients", is(nullValue())))
|
||||||
|
.andExpect(jsonPath("$.rawText", is(nullValue())))
|
||||||
|
.andExpect(jsonPath("$.owner.username", is(owner.getUsername())))
|
||||||
|
.andExpect(jsonPath("$.mainImage", is(nullValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenDraftsExist_returnDrafts() throws Exception {
|
||||||
|
final User owner = this.seedUser();
|
||||||
|
final RecipeDraft seed0 = this.recipeService.createDraft(owner);
|
||||||
|
final RecipeDraft seed1 = this.recipeService.createDraft(owner);
|
||||||
|
this.mockMvc.perform(
|
||||||
|
get("/recipe-drafts")
|
||||||
|
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$").isArray())
|
||||||
|
.andExpect(jsonPath("$", hasSize(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,7 +1,15 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
package app.mealsmadeeasy.api.recipe;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface RecipeDraftRepository extends JpaRepository<RecipeDraft, UUID> {}
|
public interface RecipeDraftRepository extends JpaRepository<RecipeDraft, UUID> {
|
||||||
|
|
||||||
|
@Query("SELECT r FROM RecipeDraft r WHERE :user = r.owner")
|
||||||
|
List<RecipeDraft> findAllViewableBy(User user);
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
|||||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
||||||
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
|
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.util.MustBeLoggedInException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@ -20,7 +21,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -39,28 +40,35 @@ public class RecipeDraftsController {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<RecipeDraftView>> getAllDraftsForUser(@AuthenticationPrincipal User user) {
|
||||||
|
if (user == null) {
|
||||||
|
throw new MustBeLoggedInException();
|
||||||
|
}
|
||||||
|
final List<RecipeDraft> recipeDrafts = this.recipeService.getDrafts(user);
|
||||||
|
return ResponseEntity.ok(recipeDrafts.stream().map(recipeDraft -> {
|
||||||
|
final @Nullable ImageView mainImage = this.getImageView(recipeDraft, user);
|
||||||
|
return RecipeDraftView.from(recipeDraft, mainImage);
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<RecipeDraftView> getRecipeDraft(
|
public ResponseEntity<RecipeDraftView> getRecipeDraft(
|
||||||
@PathVariable UUID id,
|
@PathVariable UUID id,
|
||||||
@AuthenticationPrincipal User viewer
|
@AuthenticationPrincipal User viewer
|
||||||
) {
|
) {
|
||||||
if (viewer == null) {
|
if (viewer == null) {
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
throw new MustBeLoggedInException();
|
||||||
}
|
|
||||||
final Optional<RecipeDraft> maybeRecipeDraft = this.recipeService.findDraftById(id, viewer);
|
|
||||||
if (maybeRecipeDraft.isEmpty()) {
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
} else {
|
|
||||||
final RecipeDraft recipeDraft = maybeRecipeDraft.get();
|
|
||||||
final @Nullable ImageView imageView = this.getImageView(recipeDraft, viewer);
|
|
||||||
return ResponseEntity.ok(RecipeDraftView.from(recipeDraft, imageView));
|
|
||||||
}
|
}
|
||||||
|
final RecipeDraft recipeDraft = this.recipeService.getDraftByIdWithViewer(id, viewer);
|
||||||
|
final @Nullable ImageView imageView = this.getImageView(recipeDraft, viewer);
|
||||||
|
return ResponseEntity.ok(RecipeDraftView.from(recipeDraft, imageView));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/manual")
|
@PostMapping("/manual")
|
||||||
public ResponseEntity<RecipeDraftView> createManualDraft(@AuthenticationPrincipal User owner) {
|
public ResponseEntity<RecipeDraftView> createManualDraft(@AuthenticationPrincipal User owner) {
|
||||||
if (owner == null) {
|
if (owner == null) {
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||||
final ImageView imageView = this.getImageView(recipeDraft, owner);
|
final ImageView imageView = this.getImageView(recipeDraft, owner);
|
||||||
@ -74,7 +82,7 @@ public class RecipeDraftsController {
|
|||||||
@RequestParam String sourceFileName
|
@RequestParam String sourceFileName
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
if (owner == null) {
|
if (owner == null) {
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final File file = this.fileService.create(
|
final File file = this.fileService.create(
|
||||||
sourceFile.getInputStream(),
|
sourceFile.getInputStream(),
|
||||||
@ -94,7 +102,7 @@ public class RecipeDraftsController {
|
|||||||
@RequestBody RecipeDraftUpdateBody updateBody
|
@RequestBody RecipeDraftUpdateBody updateBody
|
||||||
) throws ImageException {
|
) throws ImageException {
|
||||||
if (modifier == null) {
|
if (modifier == null) {
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final RecipeDraftUpdateSpec spec = this.updateBodyToSpecConverter.convert(updateBody, modifier);
|
final RecipeDraftUpdateSpec spec = this.updateBodyToSpecConverter.convert(updateBody, modifier);
|
||||||
final RecipeDraft updated = this.recipeService.updateDraft(id, spec, modifier);
|
final RecipeDraft updated = this.recipeService.updateDraft(id, spec, modifier);
|
||||||
@ -108,7 +116,7 @@ public class RecipeDraftsController {
|
|||||||
@PathVariable UUID id
|
@PathVariable UUID id
|
||||||
) {
|
) {
|
||||||
if (modifier == null) {
|
if (modifier == null) {
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
this.recipeService.deleteDraft(id, modifier);
|
this.recipeService.deleteDraft(id, modifier);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
@ -119,6 +127,9 @@ public class RecipeDraftsController {
|
|||||||
@AuthenticationPrincipal User modifier,
|
@AuthenticationPrincipal User modifier,
|
||||||
@PathVariable UUID id
|
@PathVariable UUID id
|
||||||
) {
|
) {
|
||||||
|
if (modifier == null) {
|
||||||
|
throw new MustBeLoggedInException();
|
||||||
|
}
|
||||||
final Recipe recipe = this.recipeService.publishDraft(id, modifier);
|
final Recipe recipe = this.recipeService.publishDraft(id, modifier);
|
||||||
final FullRecipeView view = this.recipeService.toFullRecipeView(recipe, false, modifier);
|
final FullRecipeView view = this.recipeService.toFullRecipeView(recipe, false, modifier);
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(view);
|
return ResponseEntity.status(HttpStatus.CREATED).body(view);
|
||||||
|
|||||||
@ -31,7 +31,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.*;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class RecipeService {
|
public class RecipeService {
|
||||||
@ -325,6 +328,10 @@ public class RecipeService {
|
|||||||
return viewer.getUsername().equals(username);
|
return viewer.getUsername().equals(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<RecipeDraft> getDrafts(User viewer) {
|
||||||
|
return this.recipeDraftRepository.findAllViewableBy(viewer);
|
||||||
|
}
|
||||||
|
|
||||||
public RecipeDraft createDraft(User owner) {
|
public RecipeDraft createDraft(User owner) {
|
||||||
final var recipeDraft = new RecipeDraft();
|
final var recipeDraft = new RecipeDraft();
|
||||||
recipeDraft.setState(RecipeDraft.State.ENTER_DATA);
|
recipeDraft.setState(RecipeDraft.State.ENTER_DATA);
|
||||||
@ -358,9 +365,10 @@ public class RecipeService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostAuthorize("@recipeDraftSecurity.isViewableBy(#returnObject, #viewer)")
|
@PostAuthorize("@recipeDraftSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
public Optional<RecipeDraft> findDraftById(UUID id, @Nullable User viewer) {
|
public RecipeDraft getDraftByIdWithViewer(UUID id, @Nullable User viewer) {
|
||||||
return this.recipeDraftRepository.findById(id);
|
return this.recipeDraftRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new NoSuchEntityWithIdException(RecipeDraft.class, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipeDraft saveDraft(RecipeDraft recipeDraft) {
|
public RecipeDraft saveDraft(RecipeDraft recipeDraft) {
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
package app.mealsmadeeasy.api.util;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class ExceptionHandlers {
|
||||||
|
|
||||||
|
public record NoSuchEntityWithIdExceptionView<T>(String entityName, T id, String message) {}
|
||||||
|
|
||||||
|
@ExceptionHandler(NoSuchEntityWithIdException.class)
|
||||||
|
public ResponseEntity<NoSuchEntityWithIdExceptionView<?>> handleNoSuchEntityWithIdException(
|
||||||
|
NoSuchEntityWithIdException e
|
||||||
|
) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
|
.body(new NoSuchEntityWithIdExceptionView<>(
|
||||||
|
e.getEntityType().getSimpleName(),
|
||||||
|
e.getId(),
|
||||||
|
"Could not find " + e.getEntityType().getSimpleName() + " with id " + e.getId()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record MustBeLoggedInExceptionView(String message) {}
|
||||||
|
|
||||||
|
@ExceptionHandler(MustBeLoggedInException.class)
|
||||||
|
public ResponseEntity<MustBeLoggedInExceptionView> handleMustBeLoggedInException(
|
||||||
|
MustBeLoggedInException e
|
||||||
|
) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(new MustBeLoggedInExceptionView(e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package app.mealsmadeeasy.api.util;
|
||||||
|
|
||||||
|
public class MustBeLoggedInException extends RuntimeException {
|
||||||
|
|
||||||
|
public MustBeLoggedInException() {
|
||||||
|
super("Must be logged in to perform the requested operation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public MustBeLoggedInException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user