meals-made-easy-api/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipesControllerTests.java
2026-01-24 13:29:26 -06:00

332 lines
14 KiB
Java

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("<h1>Hello, World!</h1>"))
.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("<h1>Hello, Updated World!</h1>"))
.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());
}
}