All ImageControllerTests passing.

This commit is contained in:
Jesse Brault 2024-07-26 20:04:20 -05:00
parent 6e29ec7d58
commit 20cfaa116e
8 changed files with 238 additions and 33 deletions

View File

@ -2,15 +2,18 @@ package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.auth.AuthService;
import app.mealsmadeeasy.api.auth.LoginException;
import app.mealsmadeeasy.api.image.body.ImageUpdateInfoBody;
import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec;
import app.mealsmadeeasy.api.image.spec.ImageUpdateInfoSpec;
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.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.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry;
@ -24,11 +27,12 @@ import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
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
@ -66,6 +70,9 @@ public class ImageControllerTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
private User createTestUser(String username) {
try {
return this.userService.createUser(username, username + "@test.com", "test");
@ -183,62 +190,178 @@ public class ImageControllerTests {
.andExpect(jsonPath("$.caption").value("HAL 9000, from 2001: A Space Odyssey"))
.andExpect(jsonPath("$.isPublic").value(true))
.andExpect(jsonPath("$.owner.username").value("imageOwner"))
.andExpect(jsonPath("$.owner.id").value(owner.getId()));
.andExpect(jsonPath("$.owner.id").value(owner.getId()))
.andExpect(jsonPath("$.viewers").value(empty()));
}
}
private String prepUpdate() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
this.createHal9000(owner);
return this.getAccessToken(owner.getUsername());
}
@Test
@DirtiesContext
public void updateAlt() throws Exception {
fail("TODO");
final String accessToken = this.prepUpdate();
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setAlt("HAL 9000");
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.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
@DirtiesContext
public void updateCaption() throws Exception {
fail("TODO");
final String accessToken = this.prepUpdate();
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setCaption("HAL 9000 from 2001: A Space Odyssey");
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.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
@DirtiesContext
public void updateIsPublic() throws Exception {
fail("TODO");
final String accessToken = this.prepUpdate();
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setPublic(true);
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.modified").value(notNullValue()))
.andExpect(jsonPath("$.isPublic").value(true));
}
@Test
@DirtiesContext
public void addViewers() throws Exception {
fail("TODO");
final String accessToken = this.prepUpdate();
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
final Set<String> viewerUsernames = Set.of(this.createTestUser("imageViewer")).stream()
.map(User::getUsername)
.collect(Collectors.toSet());
body.setViewersToAdd(viewerUsernames);
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.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("imageViewer"));
}
private record OwnerViewerImage(User owner, User viewer, Image image) {}
private OwnerViewerImage prepOwnerViewerImage() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("imageViewer");
final Image image = this.createHal9000(owner);
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewer));
this.imageService.update(image, owner, spec);
return new OwnerViewerImage(owner, viewer, image);
}
@Test
@DirtiesContext
public void removeViewers() throws Exception {
fail("TODO");
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.owner().getUsername());
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setViewersToRemove(Set.of(ownerViewerImage.viewer().getUsername()));
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.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
@DirtiesContext
public void clearAllViewers() throws Exception {
fail("TODO");
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.owner().getUsername());
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setClearAllViewers(true);
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.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
@DirtiesContext
public void updateInfoWithViewerFails() throws Exception {
fail("TODO");
public void updateInfoByViewerForbidden() throws Exception {
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.viewer().getUsername()); // viewer
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.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
@DirtiesContext
public void deleteImageWithOwner() throws Exception {
fail("TODO");
final User owner = this.createTestUser("imageOwner");
final Image image = this.createHal9000(owner);
final String accessToken = this.getAccessToken(owner.getUsername());
this.mockMvc.perform(
delete("/images/imageOwner/HAL9000.svg")
.header("Authorization", "Bearer " + accessToken)
)
.andExpect(status().isNoContent());
assertThrows(ImageException.class, () -> this.imageService.getById(image.getId(), owner));
}
@Test
@DirtiesContext
public void deleteImageWithViewer() throws Exception {
fail("TODO");
public void deleteImageByViewerForbidden() throws Exception {
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.viewer().getUsername());
this.mockMvc.perform(
delete("/images/imageOwner/HAL9000.svg")
.header("Authorization", "Bearer " + accessToken)
)
.andExpect(status().isForbidden());
}
}

View File

@ -7,16 +7,21 @@ import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserService;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import app.mealsmadeeasy.api.util.AccessDeniedView;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@ -39,6 +44,15 @@ public class ImageController {
userInfoView.setUsername(owner.getUsername());
imageView.setOwner(userInfoView);
final Set<UserInfoView> viewers = new HashSet<>();
for (final User viewer : image.getViewers()) {
final UserInfoView viewerView = new UserInfoView();
viewerView.setId(viewer.getId());
viewerView.setUsername(viewer.getUsername());
viewers.add(viewerView);
}
imageView.setViewers(viewers);
return imageView;
}
@ -73,6 +87,19 @@ public class ImageController {
return spec;
}
@ExceptionHandler
public ResponseEntity<AccessDeniedView> onAccessDenied(AccessDeniedException e) {
if (e instanceof AuthorizationDeniedException) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.contentType(MediaType.APPLICATION_JSON)
.body(new AccessDeniedView(HttpStatus.FORBIDDEN.value(), e.getMessage()));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.contentType(MediaType.APPLICATION_JSON)
.body(new AccessDeniedView(HttpStatus.UNAUTHORIZED.value(), e.getMessage()));
}
}
@GetMapping("/{username}/{filename}")
public ResponseEntity<InputStreamResource> getImage(
@AuthenticationPrincipal User principal,

View File

@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -64,21 +65,30 @@ public class S3ImageService implements ImageService {
};
}
private void transferFromSpec(S3ImageEntity entity, ImageCreateInfoSpec spec) {
private boolean transferFromSpec(S3ImageEntity entity, ImageCreateInfoSpec spec) {
boolean didTransfer = false;
if (spec.getAlt() != null) {
entity.setAlt(spec.getAlt());
didTransfer = true;
}
if (spec.getCaption() != null) {
entity.setCaption(spec.getCaption());
didTransfer = true;
}
if (spec.getPublic() != null) {
entity.setPublic(spec.getPublic());
didTransfer = true;
}
final @Nullable Set<User> viewersToAdd = spec.getViewersToAdd();
if (viewersToAdd != null) {
final Set<UserEntity> viewers = new HashSet<>(entity.getViewerEntities());
for (final User viewerToAdd : spec.getViewersToAdd()) {
viewers.add((UserEntity) viewerToAdd);
}
entity.setViewers(viewers);
didTransfer = true;
}
return didTransfer;
}
@Override
@ -139,16 +149,24 @@ public class S3ImageService implements ImageService {
@PreAuthorize("@imageSecurity.isOwner(#image, #modifier)")
public Image update(final Image image, User modifier, ImageUpdateInfoSpec updateSpec) {
S3ImageEntity entity = (S3ImageEntity) image;
this.transferFromSpec(entity, updateSpec);
boolean didUpdate = this.transferFromSpec(entity, updateSpec);
final @Nullable Boolean clearAllViewers = updateSpec.getClearAllViewers();
if (clearAllViewers != null && clearAllViewers) {
entity.setViewers(Set.of());
entity.setViewers(new HashSet<>());
didUpdate = true;
} else {
final Set<UserEntity> viewers = new HashSet<>(entity.getViewerEntities());
final @Nullable Set<User> viewersToRemove = updateSpec.getViewersToRemove();
if (viewersToRemove != null) {
final Set<UserEntity> currentViewers = new HashSet<>(entity.getViewerEntities());
for (final User toRemove : updateSpec.getViewersToRemove()) {
viewers.remove((UserEntity) toRemove);
currentViewers.remove((UserEntity) toRemove);
}
entity.setViewers(viewers);
entity.setViewers(currentViewers);
didUpdate = true;
}
}
if (didUpdate) {
entity.setModified(LocalDateTime.now());
}
return this.imageRepository.save(entity);
}

View File

@ -11,7 +11,7 @@ public class ImageCreateInfoSpec {
private @Nullable String alt;
private @Nullable String caption;
private @Nullable Boolean isPublic;
private Set<User> viewersToAdd = new HashSet<>();
private @Nullable Set<User> viewersToAdd = new HashSet<>();
public @Nullable String getAlt() {
return this.alt;
@ -37,11 +37,11 @@ public class ImageCreateInfoSpec {
isPublic = aPublic;
}
public Set<User> getViewersToAdd() {
public @Nullable Set<User> getViewersToAdd() {
return this.viewersToAdd;
}
public void setViewersToAdd(Set<User> viewersToAdd) {
public void setViewersToAdd(@Nullable Set<User> viewersToAdd) {
this.viewersToAdd = viewersToAdd;
}

View File

@ -8,14 +8,14 @@ import java.util.Set;
public class ImageUpdateInfoSpec extends ImageCreateInfoSpec {
private Set<User> viewersToRemove = new HashSet<>();
private @Nullable Set<User> viewersToRemove;
private @Nullable Boolean clearAllViewers;
public Set<User> getViewersToRemove() {
public @Nullable Set<User> getViewersToRemove() {
return this.viewersToRemove;
}
public void setViewersToRemove(Set<User> viewersToRemove) {
public void setViewersToRemove(@Nullable Set<User> viewersToRemove) {
this.viewersToRemove = viewersToRemove;
}

View File

@ -0,0 +1,6 @@
package app.mealsmadeeasy.api.image.view;
public class ImageExceptionView {
}

View File

@ -4,6 +4,7 @@ import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.util.Set;
public class ImageView {
@ -15,6 +16,7 @@ public class ImageView {
private @Nullable String caption;
private UserInfoView owner;
private boolean isPublic;
private Set<UserInfoView> viewers;
public LocalDateTime getCreated() {
return this.created;
@ -80,4 +82,12 @@ public class ImageView {
this.isPublic = isPublic;
}
public Set<UserInfoView> getViewers() {
return this.viewers;
}
public void setViewers(Set<UserInfoView> viewers) {
this.viewers = viewers;
}
}

View File

@ -0,0 +1,21 @@
package app.mealsmadeeasy.api.util;
public final class AccessDeniedView {
private final int statusCode;
private final String message;
public AccessDeniedView(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}
public int getStatusCode() {
return this.statusCode;
}
public String getMessage() {
return this.message;
}
}