ImageController updateInfo and deleteImage methods and related. A bunch of TODO tests.

This commit is contained in:
Jesse Brault 2024-07-26 10:12:14 -05:00
parent 9976b7337f
commit a5c0add82b
9 changed files with 378 additions and 111 deletions

View File

@ -2,6 +2,8 @@ package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.auth.AuthService;
import app.mealsmadeeasy.api.auth.LoginException;
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;
@ -21,8 +23,10 @@ import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
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.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ -76,7 +80,8 @@ public class ImageControllerTests {
owner,
USER_FILENAME,
hal9000,
27881L
27881L,
new ImageCreateInfoSpec()
);
}
}
@ -89,12 +94,24 @@ public class ImageControllerTests {
}
}
private Image makePublic(Image image, User modifier) {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setPublic(true);
return this.imageService.update(image, modifier, spec);
}
private Image addViewer(Image image, User modifier, User viewerToAdd) {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewerToAdd));
return this.imageService.update(image, modifier, spec);
}
@Test
@DirtiesContext
public void getImageNoPrincipal() throws Exception {
final User owner = this.createTestUser("imageOwner");
final Image image = this.createHal9000(owner);
this.imageService.setPublic(image, owner, true);
this.makePublic(image, owner);
try (final InputStream hal9000 = getHal9000()) {
final byte[] halBytes = hal9000.readAllBytes();
this.mockMvc.perform(get("/images/imageOwner/HAL9000.svg"))
@ -130,7 +147,7 @@ public class ImageControllerTests {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("viewer");
final Image image = this.createHal9000(owner);
this.imageService.addViewer(image, owner, viewer);
this.addViewer(image, owner, viewer);
final String accessToken = this.getAccessToken(viewer.getUsername());
this.doGetImageTestWithViewer(accessToken);
}
@ -170,4 +187,58 @@ public class ImageControllerTests {
}
}
@Test
@DirtiesContext
public void updateAlt() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void updateCaption() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void updateIsPublic() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void addViewers() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void removeViewers() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void clearAllViewers() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void updateInfoWithViewerFails() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void deleteImageWithOwner() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void deleteImageWithViewer() throws Exception {
fail("TODO");
}
}

View File

@ -1,5 +1,7 @@
package app.mealsmadeeasy.api.image;
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;
@ -17,6 +19,7 @@ import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Set;
import static app.mealsmadeeasy.api.image.ContainsImagesMatcher.containsImages;
import static app.mealsmadeeasy.api.user.IsUserMatcher.isUser;
@ -25,6 +28,7 @@ import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.fail;
@Testcontainers
@SpringBootTest
@ -68,11 +72,18 @@ public class S3ImageServiceTests {
owner,
USER_FILENAME,
hal9000,
27881L
27881L,
new ImageCreateInfoSpec()
);
}
}
private Image makePublic(Image image, User modifier) {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setPublic(true);
return this.imageService.update(image, modifier, spec);
}
@Test
public void smokeScreen() {}
@ -109,7 +120,7 @@ public class S3ImageServiceTests {
public void loadPublicImage() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
Image image = this.createHal9000(owner);
image = this.imageService.setPublic(image, owner, true);
image = this.makePublic(image, owner);
try (final InputStream stored =
this.imageService.getImageContent(image, null)) {
final byte[] storedBytes = stored.readAllBytes();
@ -123,7 +134,9 @@ public class S3ImageServiceTests {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("imageViewer");
Image image = this.createHal9000(owner);
image = this.imageService.addViewer(image, owner, viewer);
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewer));
image = this.imageService.update(image, owner, spec);
try (final InputStream stored =
this.imageService.getImageContent(image, viewer)) {
final byte[] storedBytes = stored.readAllBytes();
@ -151,40 +164,44 @@ public class S3ImageServiceTests {
@Test
@DirtiesContext
public void updateOwner() throws ImageException, IOException {
final User oldOwner = this.createTestUser("oldImageOwner");
final User newOwner = this.createTestUser("newImageOwner");
Image image = this.createHal9000(oldOwner);
assertThat(image.getOwner(), isUser(oldOwner));
image = this.imageService.updateOwner(image, oldOwner, newOwner);
assertThat(image.getOwner(), isUser(newOwner));
public void updateAlt() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void setAlt() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
Image image = this.createHal9000(owner);
image = this.imageService.setAlt(image, owner, "Example alt.");
assertThat(image.getAlt(), is("Example alt."));
public void updateCaption() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void setCaption() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
Image image = this.createHal9000(owner);
image = this.imageService.setCaption(image, owner, "Example caption.");
assertThat(image.getCaption(), is("Example caption."));
public void updateIsPublic() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void setPublicToTrue() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
Image image = this.createHal9000(owner);
image = this.imageService.setPublic(image, owner, true);
assertThat(image.isPublic(), is(true));
public void addViewers() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void removeViewers() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void clearAllViewers() throws Exception {
fail("TODO");
}
@Test
@DirtiesContext
public void deleteImage() throws Exception {
fail("TODO");
}
}

View File

@ -3,6 +3,7 @@ package app.mealsmadeeasy.api;
import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.image.ImageException;
import app.mealsmadeeasy.api.image.ImageService;
import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.RecipeService;
import app.mealsmadeeasy.api.user.User;
@ -47,13 +48,15 @@ public class DevConfiguration {
logger.info("Created {}", recipe);
try (final InputStream inputStream = DevConfiguration.class.getResourceAsStream("HAL9000.svg")) {
final ImageCreateInfoSpec spec = new ImageCreateInfoSpec();
spec.setPublic(true);
final Image image = this.imageService.create(
testUser,
"HAL9000.svg",
inputStream,
27881L
27881L,
spec
);
this.imageService.setPublic(image, testUser, true);
logger.info("Created {}", image);
} catch (IOException | ImageException e) {
logger.error("Failed to load and/or create HAL9000.svg", e);

View File

@ -1,5 +1,8 @@
package app.mealsmadeeasy.api.image;
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.image.view.ImageView;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserService;
@ -14,6 +17,8 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/images")
@ -22,6 +27,7 @@ public class ImageController {
private static ImageView getView(Image image, User owner) {
final ImageView imageView = new ImageView();
imageView.setCreated(image.getCreated());
imageView.setModified(image.getModified());
imageView.setFilename(image.getUserFilename());
imageView.setMimeType(image.getMimeType());
imageView.setAlt(image.getAlt());
@ -44,6 +50,29 @@ public class ImageController {
this.userService = userService;
}
private ImageUpdateInfoSpec getImageUpdateSpec(ImageUpdateInfoBody body) {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setAlt(body.getAlt());
spec.setCaption(body.getCaption());
spec.setPublic(body.getPublic());
if (body.getViewersToAdd() != null) {
spec.setViewersToAdd(
body.getViewersToAdd().stream()
.map(this.userService::getUser)
.collect(Collectors.toSet())
);
}
if (body.getViewersToRemove() != null) {
spec.setViewersToRemove(
body.getViewersToRemove().stream()
.map(this.userService::getUser)
.collect(Collectors.toSet())
);
}
spec.setClearAllViewers(body.getClearAllViewers());
return spec;
}
@GetMapping("/{username}/{filename}")
public ResponseEntity<InputStreamResource> getImage(
@AuthenticationPrincipal User principal,
@ -65,29 +94,58 @@ public class ImageController {
@RequestParam(required = false) String alt,
@RequestParam(required = false) String caption,
@RequestParam(required = false) Boolean isPublic,
@RequestParam(required = false) Set<String> viewers,
@AuthenticationPrincipal User principal
) throws IOException, ImageException {
if (principal == null) {
throw new AccessDeniedException("Must be logged in.");
}
Image saved = this.imageService.create(
final ImageCreateInfoSpec createSpec = new ImageCreateInfoSpec();
createSpec.setAlt(alt);
createSpec.setCaption(caption);
createSpec.setPublic(isPublic);
if (viewers != null) {
createSpec.setViewersToAdd(viewers.stream().map(this.userService::getUser).collect(Collectors.toSet()));
}
final Image saved = this.imageService.create(
principal,
filename,
image.getInputStream(),
image.getSize()
image.getSize(),
createSpec
);
if (alt != null) {
saved = this.imageService.setAlt(saved, principal, alt);
}
if (caption != null) {
saved = this.imageService.setCaption(saved, principal, caption);
}
if (isPublic != null) {
saved = this.imageService.setPublic(saved, principal, isPublic);
}
return ResponseEntity.status(201).body(getView(saved, principal));
}
@PostMapping("/{username}/{filename}")
public ResponseEntity<ImageView> updateInfo(
@AuthenticationPrincipal User principal,
@PathVariable String username,
@PathVariable String filename,
@RequestBody ImageUpdateInfoBody body
) throws ImageException {
if (principal == null) {
throw new AccessDeniedException("Must be logged in.");
}
final User owner = this.userService.getUser(username);
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
final Image updated = this.imageService.update(image, principal, this.getImageUpdateSpec(body));
return ResponseEntity.ok(getView(updated, owner));
}
@DeleteMapping("/{username}/{filename}")
public ResponseEntity<Object> deleteImage(
@AuthenticationPrincipal User principal,
@PathVariable String username,
@PathVariable String filename
) throws ImageException, IOException {
if (principal == null) {
throw new AccessDeniedException("Must be logged in.");
}
final User owner = this.userService.getUser(username);
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
this.imageService.deleteImage(image, principal);
return ResponseEntity.noContent().build();
}
}

View File

@ -1,5 +1,7 @@
package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec;
import app.mealsmadeeasy.api.image.spec.ImageUpdateInfoSpec;
import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
@ -9,23 +11,15 @@ import java.util.List;
public interface ImageService {
Image create(User owner, String userFilename, InputStream inputStream, long objectSize)
Image create(User owner, String userFilename, InputStream inputStream, long objectSize, ImageCreateInfoSpec infoSpec)
throws IOException, ImageException;
Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException;
InputStream getImageContent(Image image, @Nullable User viewer) throws IOException;
List<Image> getImagesOwnedBy(User user);
Image updateOwner(Image image, User oldOwner, User newOwner);
Image update(Image image, User modifier, ImageUpdateInfoSpec spec);
Image setAlt(Image image, User owner, String alt);
Image setCaption(Image image, User owner, String caption);
Image setPublic(Image image, User owner, boolean isPublic);
Image addViewer(Image image, User owner, User viewer);
Image removeViewer(Image image, User owner, User viewer);
Image clearViewers(Image image, User owner);
void deleteImage(Image image, User owner) throws IOException;
void deleteImage(Image image, User modifier) throws IOException;
}

View File

@ -1,5 +1,7 @@
package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec;
import app.mealsmadeeasy.api.image.spec.ImageUpdateInfoSpec;
import app.mealsmadeeasy.api.s3.S3Manager;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
@ -17,6 +19,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
/* TODO: update modified LocalDateTime when updating */
public class S3ImageService implements ImageService {
private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$");
@ -62,9 +65,29 @@ public class S3ImageService implements ImageService {
};
}
private void transferFromSpec(S3ImageEntity entity, ImageCreateInfoSpec spec) {
if (spec.getAlt() != null) {
entity.setAlt(spec.getAlt());
}
if (spec.getCaption() != null) {
entity.setCaption(spec.getCaption());
}
if (spec.getPublic() != null) {
entity.setPublic(spec.getPublic());
}
for (final User viewerToAdd : spec.getViewersToAdd()) {
entity.getViewers().add((UserEntity) viewerToAdd);
}
}
@Override
public Image create(User owner, String userFilename, InputStream inputStream, long objectSize)
throws IOException, ImageException {
public Image create(
User owner,
String userFilename,
InputStream inputStream,
long objectSize,
ImageCreateInfoSpec createSpec
) throws IOException, ImageException {
final String mimeType = this.getMimeType(userFilename);
final String uuid = UUID.randomUUID().toString();
final String extension = this.getExtension(mimeType);
@ -78,6 +101,7 @@ public class S3ImageService implements ImageService {
draft.setUserFilename(userFilename);
draft.setMimeType(mimeType);
draft.setObjectName(objectName);
this.transferFromSpec(draft, createSpec);
return this.imageRepository.save(draft);
}
@ -103,64 +127,22 @@ public class S3ImageService implements ImageService {
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #oldOwner)")
public Image updateOwner(Image image, User oldOwner, User newOwner) {
final S3ImageEntity imageEntity = (S3ImageEntity) image;
imageEntity.setOwner((UserEntity) newOwner);
return this.imageRepository.save(imageEntity);
@PreAuthorize("@imageSecurity.isOwner(#image, #modifier)")
public Image update(final Image image, User modifier, ImageUpdateInfoSpec updateSpec) {
S3ImageEntity entity = (S3ImageEntity) image;
this.transferFromSpec(entity, updateSpec);
for (final User toRemove : updateSpec.getViewersToRemove()) {
entity.getViewers().remove((UserEntity) toRemove);
}
if (updateSpec.getClearAllViewers() != null) {
entity.getViewers().clear();
}
return this.imageRepository.save(entity);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #owner)")
public Image setAlt(Image image, User owner, String alt) {
final S3ImageEntity imageEntity = (S3ImageEntity) image;
imageEntity.setAlt(alt);
return this.imageRepository.save(imageEntity);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #owner)")
public Image setCaption(Image image, User owner, String caption) {
final S3ImageEntity imageEntity = (S3ImageEntity) image;
imageEntity.setCaption(caption);
return this.imageRepository.save(imageEntity);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #owner)")
public Image setPublic(Image image, User owner, boolean isPublic) {
final S3ImageEntity imageEntity = (S3ImageEntity) image;
imageEntity.setPublic(isPublic);
return this.imageRepository.save(imageEntity);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #owner)")
public Image addViewer(Image image, User owner, User viewer) {
final S3ImageEntity withViewers = this.imageRepository.getByIdWithViewers(image.getId());
withViewers.getViewers().add((UserEntity) viewer);
return this.imageRepository.save(withViewers);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #owner)")
public Image removeViewer(Image image, User owner, User viewer) {
final S3ImageEntity withViewers = this.imageRepository.getByIdWithViewers(image.getId());
withViewers.getViewers().remove((UserEntity) viewer);
return this.imageRepository.save(withViewers);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #owner)")
public Image clearViewers(Image image, User owner) {
final S3ImageEntity withViewers = this.imageRepository.getByIdWithViewers(image.getId());
withViewers.getViewers().clear();
return this.imageRepository.save(withViewers);
}
@Override
@PreAuthorize("@imageSecurity.isOwner(#image, #owner)")
public void deleteImage(Image image, User owner) throws IOException {
@PreAuthorize("@imageSecurity.isOwner(#image, #modifier)")
public void deleteImage(Image image, User modifier) throws IOException {
final S3ImageEntity imageEntity = (S3ImageEntity) image;
this.imageRepository.delete(imageEntity);
this.s3Manager.delete("images", imageEntity.getObjectName());

View File

@ -0,0 +1,64 @@
package app.mealsmadeeasy.api.image.body;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
public class ImageUpdateInfoBody {
private @Nullable String alt;
private @Nullable String caption;
private @Nullable Boolean isPublic;
private @Nullable Set<String> viewersToAdd;
private @Nullable Set<String> viewersToRemove;
private @Nullable Boolean clearAllViewers;
public @Nullable String getAlt() {
return this.alt;
}
public void setAlt(@Nullable String alt) {
this.alt = alt;
}
public @Nullable String getCaption() {
return this.caption;
}
public void setCaption(@Nullable String caption) {
this.caption = caption;
}
public @Nullable Boolean getPublic() {
return this.isPublic;
}
public void setPublic(@Nullable Boolean aPublic) {
isPublic = aPublic;
}
public @Nullable Set<String> getViewersToAdd() {
return this.viewersToAdd;
}
public void setViewersToAdd(@Nullable Set<String> viewersToAdd) {
this.viewersToAdd = viewersToAdd;
}
public @Nullable Set<String> getViewersToRemove() {
return this.viewersToRemove;
}
public void setViewersToRemove(@Nullable Set<String> viewersToRemove) {
this.viewersToRemove = viewersToRemove;
}
public @Nullable Boolean getClearAllViewers() {
return this.clearAllViewers;
}
public void setClearAllViewers(@Nullable Boolean clearAllViewers) {
this.clearAllViewers = clearAllViewers;
}
}

View File

@ -0,0 +1,48 @@
package app.mealsmadeeasy.api.image.spec;
import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
public class ImageCreateInfoSpec {
private @Nullable String alt;
private @Nullable String caption;
private @Nullable Boolean isPublic;
private Set<User> viewersToAdd = new HashSet<>();
public @Nullable String getAlt() {
return this.alt;
}
public void setAlt(@Nullable String alt) {
this.alt = alt;
}
public @Nullable String getCaption() {
return this.caption;
}
public void setCaption(@Nullable String caption) {
this.caption = caption;
}
public @Nullable Boolean getPublic() {
return this.isPublic;
}
public void setPublic(@Nullable Boolean aPublic) {
isPublic = aPublic;
}
public Set<User> getViewersToAdd() {
return this.viewersToAdd;
}
public void setViewersToAdd(Set<User> viewersToAdd) {
this.viewersToAdd = viewersToAdd;
}
}

View File

@ -0,0 +1,30 @@
package app.mealsmadeeasy.api.image.spec;
import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
public class ImageUpdateInfoSpec extends ImageCreateInfoSpec {
private Set<User> viewersToRemove = new HashSet<>();
private @Nullable Boolean clearAllViewers;
public Set<User> getViewersToRemove() {
return this.viewersToRemove;
}
public void setViewersToRemove(Set<User> viewersToRemove) {
this.viewersToRemove = viewersToRemove;
}
public @Nullable Boolean getClearAllViewers() {
return this.clearAllViewers;
}
public void setClearAllViewers(@Nullable Boolean clearAllViewers) {
this.clearAllViewers = clearAllViewers;
}
}