411 lines
17 KiB
Java
411 lines
17 KiB
Java
package app.mealsmadeeasy.api.image;
|
|
|
|
import app.mealsmadeeasy.api.IntegrationTestsExtension;
|
|
import app.mealsmadeeasy.api.auth.AuthService;
|
|
import app.mealsmadeeasy.api.auth.LoginException;
|
|
import app.mealsmadeeasy.api.image.body.ImageUpdateBody;
|
|
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
|
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
|
import app.mealsmadeeasy.api.user.User;
|
|
import app.mealsmadeeasy.api.user.UserCreateException;
|
|
import app.mealsmadeeasy.api.user.UserService;
|
|
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.mock.web.MockMultipartFile;
|
|
import org.springframework.test.context.DynamicPropertyRegistry;
|
|
import org.springframework.test.context.DynamicPropertySource;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
import org.testcontainers.containers.MinIOContainer;
|
|
import org.testcontainers.junit.jupiter.Container;
|
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
|
import org.testcontainers.utility.DockerImageName;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
|
|
import static org.hamcrest.CoreMatchers.*;
|
|
import static org.hamcrest.Matchers.empty;
|
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
|
|
|
@Testcontainers
|
|
@SpringBootTest
|
|
@AutoConfigureMockMvc
|
|
@ExtendWith(IntegrationTestsExtension.class)
|
|
public class ImageControllerTests {
|
|
|
|
@Container
|
|
private static final MinIOContainer container = new MinIOContainer(
|
|
DockerImageName.parse("minio/minio:latest")
|
|
);
|
|
|
|
@DynamicPropertySource
|
|
public static void minioProperties(DynamicPropertyRegistry registry) {
|
|
registry.add("app.mealsmadeeasy.api.minio.endpoint", container::getS3URL);
|
|
registry.add("app.mealsmadeeasy.api.minio.accessKey", container::getUserName);
|
|
registry.add("app.mealsmadeeasy.api.minio.secretKey", container::getPassword);
|
|
}
|
|
|
|
private static InputStream getHal9000InputStream() {
|
|
return ImageControllerTests.class.getResourceAsStream("HAL9000.svg");
|
|
}
|
|
|
|
@Autowired
|
|
private UserService userService;
|
|
|
|
@Autowired
|
|
private AuthService authService;
|
|
|
|
@Autowired
|
|
private ImageService imageService;
|
|
|
|
@Autowired
|
|
private MockMvc mockMvc;
|
|
|
|
@Autowired
|
|
private ObjectMapper objectMapper;
|
|
|
|
private static final String TEST_PASSWORD = "test";
|
|
private static final long HAL_SIZE = 27881L;
|
|
|
|
private static String makeUserFilename() {
|
|
return UUID.randomUUID() + ".svg";
|
|
}
|
|
|
|
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 Image seedImage(User owner, ImageCreateSpec spec) {
|
|
try {
|
|
return this.imageService.create(
|
|
owner,
|
|
makeUserFilename(),
|
|
getHal9000InputStream(),
|
|
HAL_SIZE,
|
|
spec
|
|
);
|
|
} catch (IOException | ImageException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private Image seedImage(User owner) {
|
|
return this.seedImage(owner, ImageCreateSpec.builder().build());
|
|
}
|
|
|
|
private static String getImageUrl(User owner, Image image) {
|
|
return "/images/" + owner.getUsername() + "/" + image.getUserFilename();
|
|
}
|
|
|
|
private String getAccessToken(User user) {
|
|
try {
|
|
return this.authService.login(user.getUsername(), TEST_PASSWORD).getAccessToken().getToken();
|
|
} catch (LoginException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void getPublicImageNoPrincipal() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final ImageCreateSpec spec = ImageCreateSpec.builder()
|
|
.isPublic(true)
|
|
.build();
|
|
final Image image = this.seedImage(owner, spec);
|
|
|
|
// Assert bytes the same and proper mime type
|
|
try (final InputStream hal9000 = getHal9000InputStream()) {
|
|
final byte[] halBytes = hal9000.readAllBytes();
|
|
this.mockMvc.perform(get(getImageUrl(owner, image)))
|
|
.andExpect(status().isOk())
|
|
.andExpect(content().contentType("image/svg+xml"))
|
|
.andExpect(content().bytes(halBytes));
|
|
}
|
|
}
|
|
|
|
private void doGetImageTestWithAccessToken(User owner, Image image, String accessToken) throws Exception {
|
|
try (final InputStream hal9000 = getHal9000InputStream()) {
|
|
final byte[] halBytes = hal9000.readAllBytes();
|
|
this.mockMvc.perform(get(getImageUrl(owner, image))
|
|
.header("Authorization", "Bearer " + accessToken))
|
|
.andExpect(status().isOk())
|
|
.andExpect(content().contentType("image/svg+xml"))
|
|
.andExpect(content().bytes(halBytes));
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void getImageForOwner() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
this.doGetImageTestWithAccessToken(owner, image, this.getAccessToken(owner));
|
|
}
|
|
|
|
@Test
|
|
public void getImageForViewer() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final User viewer = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
|
|
// add viewer
|
|
final ImageUpdateSpec spec = ImageUpdateSpec.builder()
|
|
.viewersToAdd(Set.of(viewer))
|
|
.build();
|
|
this.imageService.update(image, owner, spec);
|
|
|
|
this.doGetImageTestWithAccessToken(owner, image, this.getAccessToken(viewer));
|
|
}
|
|
|
|
@Test
|
|
public void getNonPublicImageNoPrincipalForbidden() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
this.mockMvc.perform(get(getImageUrl(owner, image)))
|
|
.andExpect(status().isForbidden());
|
|
}
|
|
|
|
@Test
|
|
public void getNonPublicImageWithPrincipalForbidden() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final User nonViewer = this.seedUser();
|
|
final String nonViewerToken = this.getAccessToken(nonViewer);
|
|
this.mockMvc.perform(
|
|
get(getImageUrl(owner, image))
|
|
.header("Authorization", "Bearer " + nonViewerToken)
|
|
).andExpect(status().isForbidden());
|
|
}
|
|
|
|
@Test
|
|
public void getImageWithViewersNoPrincipalForbidden() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final User viewer = this.seedUser();
|
|
|
|
// add viewer
|
|
final ImageUpdateSpec spec = ImageUpdateSpec.builder()
|
|
.viewersToAdd(Set.of(viewer))
|
|
.build();
|
|
this.imageService.update(image, owner, spec);
|
|
|
|
this.mockMvc.perform(get(getImageUrl(owner, image))).andExpect(status().isForbidden());
|
|
}
|
|
|
|
@Test
|
|
public void putImage() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final String accessToken = this.getAccessToken(owner);
|
|
try (final InputStream hal9000 = getHal9000InputStream()) {
|
|
final String userFilename = makeUserFilename();
|
|
final MockMultipartFile mockMultipartFile = new MockMultipartFile(
|
|
"image", userFilename, "image/svg+xml", hal9000
|
|
);
|
|
this.mockMvc.perform(
|
|
multipart("/images")
|
|
.file(mockMultipartFile)
|
|
.param("filename", userFilename)
|
|
.param("alt", "HAL 9000")
|
|
.param("caption", "HAL 9000, from 2001: A Space Odyssey")
|
|
.param("isPublic", "true")
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
.with(req -> {
|
|
req.setMethod("PUT");
|
|
return req;
|
|
})
|
|
)
|
|
.andExpect(status().isCreated())
|
|
.andExpect(jsonPath("$.created").exists())
|
|
.andExpect(jsonPath("$.modified").value(nullValue()))
|
|
.andExpect(jsonPath("$.filename").value(userFilename))
|
|
.andExpect(jsonPath("$.mimeType").value("image/svg+xml"))
|
|
.andExpect(jsonPath("$.alt").value("HAL 9000"))
|
|
.andExpect(jsonPath("$.caption").value("HAL 9000, from 2001: A Space Odyssey"))
|
|
.andExpect(jsonPath("$.public").value(true))
|
|
.andExpect(jsonPath("$.owner.username").value(owner.getUsername()))
|
|
.andExpect(jsonPath("$.owner.id").value(owner.getId()))
|
|
.andExpect(jsonPath("$.viewers").value(empty()));
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void updateAlt() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final String accessToken = this.getAccessToken(owner);
|
|
final ImageUpdateBody body = new ImageUpdateBody();
|
|
body.setAlt("HAL 9000");
|
|
this.mockMvc.perform(
|
|
post(getImageUrl(owner, image))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(this.objectMapper.writeValueAsString(body))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.modified").value(notNullValue()))
|
|
.andExpect(jsonPath("$.alt").value("HAL 9000"));
|
|
}
|
|
|
|
@Test
|
|
public void updateCaption() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final String accessToken = this.getAccessToken(owner);
|
|
final ImageUpdateBody body = new ImageUpdateBody();
|
|
body.setCaption("HAL 9000 from 2001: A Space Odyssey");
|
|
this.mockMvc.perform(
|
|
post(getImageUrl(owner, image))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(this.objectMapper.writeValueAsString(body))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.modified").value(notNullValue()))
|
|
.andExpect(jsonPath("$.caption").value("HAL 9000 from 2001: A Space Odyssey"));
|
|
}
|
|
|
|
@Test
|
|
public void updateIsPublic() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final String accessToken = this.getAccessToken(owner);
|
|
final ImageUpdateBody body = new ImageUpdateBody();
|
|
body.setIsPublic(true);
|
|
this.mockMvc.perform(
|
|
post(getImageUrl(owner, image))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(this.objectMapper.writeValueAsString(body))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.modified").value(notNullValue()))
|
|
.andExpect(jsonPath("$.public").value(true));
|
|
}
|
|
|
|
@Test
|
|
public void addViewers() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final User viewerToAdd = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final String accessToken = this.getAccessToken(owner);
|
|
|
|
final ImageUpdateBody body = new ImageUpdateBody();
|
|
final Set<String> viewerUsernames = Set.of(viewerToAdd.getUsername());
|
|
body.setViewersToAdd(viewerUsernames);
|
|
|
|
this.mockMvc.perform(
|
|
post(getImageUrl(owner, image))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(this.objectMapper.writeValueAsString(body))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.modified").value(notNullValue()))
|
|
.andExpect(jsonPath("$.viewers").value(not(empty())))
|
|
.andExpect(jsonPath("$.viewers[0].username").value(viewerToAdd.getUsername()));
|
|
}
|
|
|
|
private record OwnerViewerImage(User owner, User viewer, Image image) {}
|
|
|
|
private OwnerViewerImage prepOwnerViewerImage() {
|
|
final User owner = this.seedUser();
|
|
final User viewer = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final ImageUpdateSpec spec = ImageUpdateSpec.builder()
|
|
.viewersToAdd(Set.of(viewer))
|
|
.build();
|
|
this.imageService.update(image, owner, spec);
|
|
return new OwnerViewerImage(owner, viewer, image);
|
|
}
|
|
|
|
@Test
|
|
public void removeViewers() throws Exception {
|
|
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
|
|
final String accessToken = this.getAccessToken(ownerViewerImage.owner());
|
|
|
|
final ImageUpdateBody body = new ImageUpdateBody();
|
|
body.setViewersToRemove(Set.of(ownerViewerImage.viewer().getUsername()));
|
|
|
|
this.mockMvc.perform(
|
|
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(this.objectMapper.writeValueAsString(body))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.modified").value(notNullValue()))
|
|
.andExpect(jsonPath("$.viewers").value(empty()));
|
|
}
|
|
|
|
@Test
|
|
public void clearAllViewers() throws Exception {
|
|
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
|
|
final String accessToken = this.getAccessToken(ownerViewerImage.owner());
|
|
final ImageUpdateBody body = new ImageUpdateBody();
|
|
body.setClearAllViewers(true);
|
|
this.mockMvc.perform(
|
|
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(this.objectMapper.writeValueAsString(body))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.modified").value(notNullValue()))
|
|
.andExpect(jsonPath("$.viewers").value(empty()));
|
|
}
|
|
|
|
@Test
|
|
public void updateInfoByViewerForbidden() throws Exception {
|
|
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
|
|
final String accessToken = this.getAccessToken(ownerViewerImage.viewer()); // viewer
|
|
final ImageUpdateBody body = new ImageUpdateBody();
|
|
this.mockMvc.perform(
|
|
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(this.objectMapper.writeValueAsString(body))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isForbidden())
|
|
.andExpect(jsonPath("$.statusCode").value(403))
|
|
.andExpect(jsonPath("$.message").value(notNullValue()));
|
|
}
|
|
|
|
@Test
|
|
public void deleteImageWithOwner() throws Exception {
|
|
final User owner = this.seedUser();
|
|
final Image image = this.seedImage(owner);
|
|
final String accessToken = this.getAccessToken(owner);
|
|
this.mockMvc.perform(
|
|
delete(getImageUrl(owner, image))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isNoContent());
|
|
assertThrows(ImageException.class, () -> this.imageService.getById(image.getId(), owner));
|
|
}
|
|
|
|
@Test
|
|
public void deleteImageByViewerForbidden() throws Exception {
|
|
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
|
|
final String accessToken = this.getAccessToken(ownerViewerImage.viewer());
|
|
this.mockMvc.perform(
|
|
delete(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
)
|
|
.andExpect(status().isForbidden());
|
|
}
|
|
|
|
}
|