package app.mealsmadeeasy.api.recipe; import app.mealsmadeeasy.api.PostgresTestsExtension; import app.mealsmadeeasy.api.auth.AuthService; import app.mealsmadeeasy.api.auth.LoginDetails; import app.mealsmadeeasy.api.auth.LoginException; import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.image.ImageService; import app.mealsmadeeasy.api.image.S3ImageServiceTests; import app.mealsmadeeasy.api.image.spec.ImageCreateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; import app.mealsmadeeasy.api.recipe.star.RecipeStarService; import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.UserCreateException; import app.mealsmadeeasy.api.user.UserService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; 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.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.io.InputStream; import java.util.UUID; import static org.hamcrest.Matchers.*; 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.status; @SpringBootTest @AutoConfigureMockMvc @ExtendWith(PostgresTestsExtension.class) public class RecipesControllerTests { private static InputStream getHal9000() { return S3ImageServiceTests.class.getClassLoader().getResourceAsStream("HAL9000.svg"); } @Autowired private MockMvc mockMvc; @Autowired private RecipeService recipeService; @Autowired private RecipeStarService recipeStarService; @Autowired private UserService userService; @Autowired private AuthService authService; @Autowired private ImageService imageService; @Autowired private ObjectMapper objectMapper; 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 Recipe createTestRecipe(User owner, boolean isPublic) { final RecipeCreateSpec spec = RecipeCreateSpec.builder() .slug(UUID.randomUUID().toString()) .title("Test Recipe") .preparationTime(10) .cookingTime(20) .totalTime(30) .rawText("# Hello, World!") .isPublic(isPublic) .build(); return this.recipeService.create(owner, spec); } private String getAccessToken(User user) throws LoginException { return this.authService.login(user.getUsername(), TEST_PASSWORD) .getAccessToken() .getToken(); } private Image createHal9000(User owner) { try (final InputStream hal9000 = getHal9000()) { return this.imageService.create( owner, UUID.randomUUID() + ".svg", hal9000, 27881L, ImageCreateSpec.builder().build() ); } catch (Exception e) { throw new RuntimeException(e); } } @Test public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception { final User owner = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, true); this.mockMvc.perform( get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug()) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.recipe.id").value(recipe.getId())) .andExpect(jsonPath("$.recipe.created").exists()) // TODO: better matching of exact LocalDateTime .andExpect(jsonPath("$.recipe.modified").doesNotExist()) .andExpect(jsonPath("$.recipe.slug").value(recipe.getSlug())) .andExpect(jsonPath("$.recipe.title").value("Test Recipe")) .andExpect(jsonPath("$.recipe.preparationTime").value(recipe.getPreparationTime())) .andExpect(jsonPath("$.recipe.cookingTime").value(recipe.getCookingTime())) .andExpect(jsonPath("$.recipe.totalTime").value(recipe.getTotalTime())) .andExpect(jsonPath("$.recipe.text").value("

Hello, World!

")) .andExpect(jsonPath("$.recipe.rawText").doesNotExist()) .andExpect(jsonPath("$.recipe.owner.id").value(owner.getId())) .andExpect(jsonPath("$.recipe.owner.username").value(owner.getUsername())) .andExpect(jsonPath("$.recipe.starCount").value(0)) .andExpect(jsonPath("$.recipe.viewerCount").value(0)) .andExpect(jsonPath("$.recipe.public").value(true)) .andExpect(jsonPath("$.recipe.mainImage").value(nullValue())) .andExpect(jsonPath("$.isStarred").value(nullValue())) .andExpect(jsonPath("$.isOwner").value(nullValue())); } @Test public void getFullRecipeViewIncludeRawText() throws Exception { final User owner = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, true); this.mockMvc.perform( get( "/recipes/{username}/{slug}?includeRawText=true", recipe.getOwner().getUsername(), recipe.getSlug() ) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.recipe.rawText").value(recipe.getRawText())); } @Test public void getFullRecipeViewPrincipalIsStarer() throws Exception { final User owner = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, false); this.recipeStarService.create(recipe.getId(), owner.getId()); final String accessToken = this.getAccessToken(owner); this.mockMvc.perform( get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug()) .header("Authorization", "Bearer " + accessToken) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.isStarred").value(true)); } @Test public void getFullRecipeViewPrincipalIsNotStarer() throws Exception { final User owner = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, false); final String accessToken = this.getAccessToken(owner); this.mockMvc.perform( get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug()) .header("Authorization", "Bearer " + accessToken) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.isStarred").value(false)); } @Test public void getRecipeInfoViewsNoPrincipal() throws Exception { final User owner = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, true); this.mockMvc.perform(get("/recipes")) .andExpect(status().isOk()) .andExpect(jsonPath("$.slice.number").value(0)) .andExpect(jsonPath("$.slice.size").value(20)) .andExpect(jsonPath("$.content").isArray()) .andExpect(jsonPath("$.content[*].id").value(hasItem(recipe.getId()))); } @Test public void getRecipeInfoViewsWithPrincipalIncludesPrivate() throws Exception { final User owner = this.seedUser(); final Recipe r0 = this.createTestRecipe(owner, true); final Recipe r1 = this.createTestRecipe(owner, true); final Recipe r2 = this.createTestRecipe(owner, false); final LoginDetails loginDetails = this.authService.login(owner.getUsername(), "test"); this.mockMvc.perform( get("/recipes") .header("Authorization", "Bearer " + loginDetails.getAccessToken().getToken()) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.slice.number").value(0)) .andExpect(jsonPath("$.slice.size").value(20)) .andExpect(jsonPath("$.content").isArray()) .andExpect(jsonPath("$.content[*].id").value(hasItems(r0.getId(), r1.getId(), r2.getId()))); } private String getUpdateBody() throws JsonProcessingException { final RecipeUpdateSpec spec = RecipeUpdateSpec.builder() .title("Updated Test Recipe") .preparationTime(15) .cookingTime(30) .totalTime(45) .rawText("# Hello, Updated World!") .isPublic(true) .build(); return this.objectMapper.writeValueAsString(spec); } @Test public void updateRecipe() throws Exception { final User owner = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, false); final String accessToken = this.getAccessToken(owner); final String body = this.getUpdateBody(); this.mockMvc.perform( post("/recipes/{username}/{slug}", owner.getUsername(), recipe.getSlug()) .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(body) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.recipe.id").value(recipe.getId())) .andExpect(jsonPath("$.recipe.title").value("Updated Test Recipe")) .andExpect(jsonPath("$.recipe.preparationTime").value(15)) .andExpect(jsonPath("$.recipe.cookingTime").value(30)) .andExpect(jsonPath("$.recipe.totalTime").value(45)) .andExpect(jsonPath("$.recipe.text").value("

Hello, Updated World!

")) .andExpect(jsonPath("$.recipe.rawText").value("# Hello, Updated World!")) .andExpect(jsonPath("$.recipe.owner.id").value(owner.getId())) .andExpect(jsonPath("$.recipe.owner.username").value(owner.getUsername())) .andExpect(jsonPath("$.recipe.starCount").value(0)) .andExpect(jsonPath("$.recipe.viewerCount").value(0)) .andExpect(jsonPath("$.recipe.public").value(true)) .andExpect(jsonPath("$.recipe.mainImage").value(nullValue())) .andExpect(jsonPath("$.isStarred").value(false)) .andExpect(jsonPath("$.isOwner").value(true)); } @Test public void updateRecipeReturnsViewWithMainImage() throws Exception { final User owner = this.seedUser(); final Image hal9000 = this.createHal9000(owner); final RecipeCreateSpec createSpec = RecipeCreateSpec.builder() .title("Test Recipe") .slug(UUID.randomUUID().toString()) .isPublic(false) .rawText("# Hello, World!") .mainImage(hal9000) .build(); Recipe recipe = this.recipeService.create(owner, createSpec); final RecipeUpdateSpec updateSpec = RecipeUpdateSpec.builder() .title("Updated Test Recipe") .rawText("# Hello, Updated World!") .mainImage( RecipeUpdateSpec.MainImageUpdateSpec.builder() .username(hal9000.getOwner().getUsername()) .filename(hal9000.getUserFilename()) .build() ) .build(); final String body = this.objectMapper.writeValueAsString(updateSpec); final String accessToken = this.getAccessToken(owner); this.mockMvc.perform( post("/recipes/{username}/{slug}", owner.getUsername(), recipe.getSlug()) .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(body) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.recipe.mainImage").isMap()); } @Test public void addStarToRecipe() throws Exception { final User owner = this.seedUser(); final User starer = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, true); this.mockMvc.perform( post("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug()) .header("Authorization", "Bearer " + this.getAccessToken(starer)) ) .andExpect(status().isCreated()) .andExpect(jsonPath("$.timestamp").exists()); } @Test public void getStarForRecipe() throws Exception { final User owner = this.seedUser(); final User starer = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, true); this.recipeStarService.create(recipe.getId(), starer.getId()); this.mockMvc.perform( get("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug()) .header("Authorization", "Bearer " + this.getAccessToken(starer)) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.isStarred").value(true)) .andExpect(jsonPath("$.star").isMap()) .andExpect(jsonPath("$.star.timestamp").exists()); } @Test public void deleteStarFromRecipe() throws Exception { final User owner = this.seedUser(); final User starer = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, true); this.recipeStarService.create(recipe.getId(), starer.getId()); this.mockMvc.perform( delete("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug()) .header("Authorization", "Bearer " + this.getAccessToken(starer)) ) .andExpect(status().isNoContent()); } }