All ImageControllerTests passing.
This commit is contained in:
parent
6e29ec7d58
commit
20cfaa116e
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
package app.mealsmadeeasy.api.image.view;
|
||||
|
||||
public class ImageExceptionView {
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user