이번 시간에는 게시글 수정 기능을 작성해보겠습니다.
entity.post.Post에 update 메소드를 추가해주겠습니다.
package kukekyakya.kukemarket.entity.post;
import ...
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends EntityDate {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
@Lob
private String content;
@Column(nullable = false)
private Long price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Category category;
@OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Image> images;
public Post(String title, String content, Long price, Member member, Category category, List<Image> images) {
this.title = title;
this.content = content;
this.price = price;
this.member = member;
this.category = category;
this.images = new ArrayList<>();
addImages(images);
}
public ImageUpdatedResult update(PostUpdateRequest req) { // 1
this.title = req.getTitle();
this.content = req.getContent();
this.price = req.getPrice();
ImageUpdatedResult result = findImageUpdatedResult(req.getAddedImages(), req.getDeletedImages());
addImages(result.getAddedImages());
deleteImages(result.getDeletedImages());
return result;
}
private void addImages(List<Image> added) {
added.stream().forEach(i -> {
images.add(i);
i.initPost(this);
});
}
private void deleteImages(List<Image> deleted) { // 2
deleted.stream().forEach(di -> this.images.remove(di));
}
// 3
private ImageUpdatedResult findImageUpdatedResult(List<MultipartFile> addedImageFiles, List<Long> deletedImageIds) {
List<Image> addedImages = convertImageFilesToImages(addedImageFiles);
List<Image> deletedImages = convertImageIdsToImages(deletedImageIds);
return new ImageUpdatedResult(addedImageFiles, addedImages, deletedImages);
}
private List<Image> convertImageIdsToImages(List<Long> imageIds) {
return imageIds.stream()
.map(id -> convertImageIdToImage(id))
.filter(i -> i.isPresent())
.map(i -> i.get())
.collect(toList());
}
private Optional<Image> convertImageIdToImage(Long id) {
return this.images.stream().filter(i -> i.getId().equals(id)).findAny();
}
private List<Image> convertImageFilesToImages(List<MultipartFile> imageFiles) {
return imageFiles.stream().map(imageFile -> new Image(imageFile.getOriginalFilename())).collect(toList());
}
@Getter
@AllArgsConstructor
public static class ImageUpdatedResult { // 4
private List<MultipartFile> addedImageFiles;
private List<Image> addedImages;
private List<Image> deletedImages;
}
}
새롭게 작성된 부분만 확인해보겠습니다.
1. PostUpdateRequest를 받아서 업데이트를 수행해줍니다. 이미지는 별도의 파일 저장소에 저장되어있으므로, 새롭게 업데이트된 이미지 결과에 대해서 파일 저장소에 반영해주어야합니다. 이를 위해 업데이트 결과로, 추가된 이미지들에 대한 정보와 삭제된 이미지들에 대한 정보를 반환해주었습니다.
2. this.images에서 삭제될 이미지를 제거해줍니다. 파라미터로 전달받은 이미지와 this.images는 영속된 상태일 것이고, orphanRemoval=true에 의해 Post와 연관 관계가 끊어지며 고아 객체가 된 Image는 데이터베이스에서도 제거될 것입니다.
3. 업데이트되어야할 이미지 결과 정보를 만들어줍니다.
4. update를 호출한 클라이언트에게 전달될 이미지 업데이트 결과입니다. 클라이언트는 이 정보를 가지고, 실제 파일 저장소에서 추가될 이미지는 업로드하고, 삭제될 이미지는 제거할 것입니다.
dto.post.PostUpdateRequest는 다음과 같습니다.
package kukekyakya.kukemarket.dto.post;
import ...
@ApiModel(value = "게시글 수정 요청")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PostUpdateRequest {
@ApiModelProperty(value = "게시글 제목", notes = "게시글 제목을 입력해주세요", required = true, example = "my title")
@NotBlank(message = "게시글 제목을 입력해주세요.")
private String title;
@ApiModelProperty(value = "게시글 본문", notes = "게시글 본문을 입력해주세요", required = true, example = "my content")
@NotBlank(message = "게시글 본문을 입력해주세요.")
private String content;
@ApiModelProperty(value = "가격", notes = "가격을 입력해주세요", required = true, example = "50000")
@NotNull(message = "가격을 입력해주세요.")
@PositiveOrZero(message = "0원 이상을 입력해주세요")
private Long price;
@ApiModelProperty(value = "추가된 이미지", notes = "추가된 이미지를 첨부해주세요.")
private List<MultipartFile> addedImages = new ArrayList<>();
@ApiModelProperty(value = "제거된 이미지 아이디", notes = "제거된 이미지 아이디를 입력해주세요.")
private List<Long> deletedImages = new ArrayList<>();
}
수정되는 게시글의 정보를 가지고 있습니다.
새롭게 추가되는 이미지는 MultipartFile로 받고, 삭제되어야하는 이미지는 이미지의 id로 받겠습니다.
이미지가 수정된다면, 새롭게 추가되는 이미지와 제거되는 이미지가 무엇인지 알아야합니다.
이러한 이미지 변경 내역을 어디에서 확인해야할지 고민이 되었는데, 결국 클라이언트에서 변경된 내역을 보내주는 것이 가장 낫다고 생각되었습니다.
클라이언트는 어떤 이미지가 추가되었고, 어떤 이미지가 제거된 것인지 알고 있습니다.
단순히 기존에 내려받은 이미지 정보에서 제거 된 이미지의 id를 기억해두고, 새롭게 추가되는 이미지만 multipart/form-data로 보내주면 되는 것입니다.
만약 모든 파일을 서버에 다시 전송하여 서버 측에서 이를 검사한다면, 이미 업로드된 이미지 데이터까지 전송되기 때문에 네트워크 대역폭을 낭비하게 되는 문제도 있고, 기존의 이미지 목록과 새롭게 전송받은 이미지 목록에서 업데이트되어야할 결과를 찾아내고 수정하는 것은 번거로운 작업입니다.
하지만 클라이언트에서 이를 구현한다면 그리 복잡한 작업은 아닐 것이고, 굳이 서버에서 검증해줘야할 내용은 아니라고 판단되어서 이러한 방법을 택하게 되었습니다.
test 디렉토리에 PostUpdateRequestValidationTest를 작성하여 검증 어노테이션을 바로 테스트해주겠습니다.
package kukekyakya.kukemarket.dto.post;
import ...
class PostUpdateRequestValidationTest {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Test
void validateTest() {
// given
PostUpdateRequest req = createPostUpdateRequest("title", "content", 1234L, List.of(), List.of());
// when
Set<ConstraintViolation<PostUpdateRequest>> validate = validator.validate(req);
// then
assertThat(validate).isEmpty();
}
@Test
void invalidateByEmptyTitleTest() {
// given
String invalidValue = null;
PostUpdateRequest req = createPostUpdateRequest(invalidValue, "content", 1234L, List.of(), List.of());
// when
Set<ConstraintViolation<PostUpdateRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankTitleTest() {
// given
String invalidValue = " ";
PostUpdateRequest req = createPostUpdateRequest(invalidValue, "content", 1234L, List.of(), List.of());
// when
Set<ConstraintViolation<PostUpdateRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByEmptyContentTest() {
// given
String invalidValue = null;
PostUpdateRequest req = createPostUpdateRequest("title", invalidValue, 1234L, List.of(), List.of());
// when
Set<ConstraintViolation<PostUpdateRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankContentTest() {
// given
String invalidValue = " ";
PostUpdateRequest req = createPostUpdateRequest("title", invalidValue, 1234L, List.of(), List.of());
// when
Set<ConstraintViolation<PostUpdateRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByNullPriceTest() {
// given
Long invalidValue = null;
PostUpdateRequest req = createPostUpdateRequest("title", "content", invalidValue, List.of(), List.of());
// when
Set<ConstraintViolation<PostUpdateRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByNegativePriceTest() {
// given
Long invalidValue = -1L;
PostUpdateRequest req = createPostUpdateRequest("title", "content", invalidValue, List.of(), List.of());
// when
Set<ConstraintViolation<PostUpdateRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
}
자세한 설명은 생략하겠습니다.
사용된 factory.dto.PostUpdateRequestFactory는 다음과 같습니다.
package kukekyakya.kukemarket.factory.dto;
import ...
public class PostUpdateRequestFactory {
public static PostUpdateRequest createPostUpdateRequest(String title, String content, Long price, List<MultipartFile> addedImages, List<Long> deletedImages) {
return new PostUpdateRequest(title, content, price, addedImages, deletedImages);
}
}
단순히 인스턴스를 생성하여 반환해줍니다.
Post에 새롭게 작성된 update도 테스트해주겠습니다.
test 디렉토리에 PostTest를 작성해줍니다.
package kukekyakya.kukemarket.entity.post;
import ...
class PostTest {
@Test
void updateTest() {
// given
Image a = createImageWithIdAndOriginName(1L, "a.jpg");
Image b = createImageWithIdAndOriginName(2L, "b.jpg");
Post post = createPostWithImages(createMember(), createCategory(), List.of(a, b));
// when
MockMultipartFile cFile = new MockMultipartFile("c", "c.png", MediaType.IMAGE_PNG_VALUE, "cFile".getBytes());
PostUpdateRequest postUpdateRequest = createPostUpdateRequest("update title", "update content", 1234L, List.of(cFile), List.of(a.getId()));
Post.ImageUpdatedResult imageUpdatedResult = post.update(postUpdateRequest);
// then
assertThat(post.getTitle()).isEqualTo(postUpdateRequest.getTitle());
assertThat(post.getContent()).isEqualTo(postUpdateRequest.getContent());
assertThat(post.getPrice()).isEqualTo(postUpdateRequest.getPrice());
List<Image> resultImages = post.getImages();
List<String> resultOriginNames = resultImages.stream().map(i -> i.getOriginName()).collect(toList());
assertThat(resultImages.size()).isEqualTo(2);
assertThat(resultOriginNames).contains(b.getOriginName(), cFile.getOriginalFilename());
List<MultipartFile> addedImageFiles = imageUpdatedResult.getAddedImageFiles();
assertThat(addedImageFiles.size()).isEqualTo(1);
assertThat(addedImageFiles.get(0).getOriginalFilename()).isEqualTo(cFile.getOriginalFilename());
List<Image> addedImages = imageUpdatedResult.getAddedImages();
List<String> addedOriginNames = addedImages.stream().map(i -> i.getOriginName()).collect(toList());
assertThat(addedImages.size()).isEqualTo(1);
assertThat(addedOriginNames).contains(cFile.getOriginalFilename());
List<Image> deletedImages = imageUpdatedResult.getDeletedImages();
List<String> deletedOriginNames = deletedImages.stream().map(i -> i.getOriginName()).collect(toList());
assertThat(deletedImages.size()).isEqualTo(1);
assertThat(deletedOriginNames).contains(a.getOriginName());
}
}
기존에 a.jpg와 b.jpg라는 두 개의 이미지가 있고, c.png라는 이미지는 새롭게 추가되면서 a.jpg 이미지는 제거하도록 하였습니다. 업데이트 내역에 대해 응답받은 ImageUpdatedResult를 이용하여 검증해주었습니다.
JPA와 연동하여 함께 테스트해보겠습니다.
Post에 업데이트된 내역이 데이터베이스에도 반영되어야합니다.
PostRepositoryTest에 다음 테스트를 추가 작성해줍니다.
// PostRepositoryTest.java
@Test
void updateTest() {
// given
Image a = createImageWithOriginName("a.jpg");
Image b = createImageWithOriginName("b.png");
Post post = postRepository.save(createPostWithImages(member, category, List.of(a, b)));
clear();
// when
MockMultipartFile cFile = new MockMultipartFile("c", "c.png", MediaType.IMAGE_PNG_VALUE, "cFile".getBytes());
PostUpdateRequest postUpdateRequest = createPostUpdateRequest("update title", "update content", 1234L, List.of(cFile), List.of(a.getId()));
Post foundPost = postRepository.findById(post.getId()).orElseThrow(PostNotFoundException::new);
foundPost.update(postUpdateRequest);
clear();
// then
Post result = postRepository.findById(post.getId()).orElseThrow(PostNotFoundException::new);
assertThat(result.getTitle()).isEqualTo(postUpdateRequest.getTitle());
assertThat(result.getContent()).isEqualTo(postUpdateRequest.getContent());
assertThat(result.getPrice()).isEqualTo(postUpdateRequest.getPrice());
List<Image> images = result.getImages();
List<String> originNames = images.stream().map(i -> i.getOriginName()).collect(toList());
assertThat(images.size()).isEqualTo(2);
assertThat(originNames).contains(b.getOriginName(), cFile.getOriginalFilename());
List<Image> resultImages = imageRepository.findAll();
assertThat(resultImages.size()).isEqualTo(2);
}
PostTest와 동일한 상황을 테스트하면서, 실제 데이터베이스에도 반영되었는지 확인해주었습니다.
이제 서비스 로직을 작성해주겠습니다.
service.post.PostService에 다음과 같은 메소드를 추가해줍니다.
// PostService.java
@Transactional
public PostUpdateResponse update(Long id, PostUpdateRequest req) {
Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new);
Post.ImageUpdatedResult result = post.update(req);
uploadImages(result.getAddedImages(), result.getAddedImageFiles());
deleteImages(result.getDeletedImages());
return new PostUpdateResponse(id);
}
private void uploadImages(List<Image> images, List<MultipartFile> fileImages) {
IntStream.range(0, images.size()).forEach(i -> fileService.upload(fileImages.get(i), images.get(i).getUniqueName()));
}
private void deleteImages(List<Image> images) {
images.stream().forEach(i -> fileService.delete(i.getUniqueName()));
}
단순히 전달받은 PostUpdateRequest를 이용하여 Post.update를 수행하고,
이미지 업데이트 결과로 응답받은 ImageUpdatedResult를 이용하여 실제 파일 저장소도 업데이트해주었습니다.
dto.post.PostUpdateResponse는 다음과 같습니다.
package kukekyakya.kukemarket.dto.post;
import ...
@Data
@AllArgsConstructor
public class PostUpdateResponse {
private Long id;
}
단순히 업데이트된 게시글의 id만 가지고 있습니다.
이제 PostService의 수정 기능도 테스트해주겠습니다.
PostServiceTest에 다음과 같은 테스트를 추가해줍니다.
// PostServiceTest.java
@Test
void updateTest() {
// given
Image a = createImageWithIdAndOriginName(1L, "a.png");
Image b = createImageWithIdAndOriginName(2L, "b.png");
Post post = createPostWithImages(List.of(a, b));
given(postRepository.findById(anyLong())).willReturn(Optional.of(post));
MockMultipartFile cFile = new MockMultipartFile("c", "c.png", MediaType.IMAGE_PNG_VALUE, "c".getBytes());
PostUpdateRequest postUpdateRequest = createPostUpdateRequest("title", "content", 1000L, List.of(cFile), List.of(a.getId()));
// when
postService.update(1L, postUpdateRequest);
// then
List<Image> images = post.getImages();
List<String> originNames = images.stream().map(i -> i.getOriginName()).collect(toList());
assertThat(originNames.size()).isEqualTo(2);
assertThat(originNames).contains(b.getOriginName(), cFile.getOriginalFilename());
verify(fileService, times(1)).upload(any(), anyString());
verify(fileService, times(1)).delete(anyString());
}
@Test
void updateExceptionByPostNotFoundTest() {
// given
given(postRepository.findById(anyLong())).willReturn(Optional.ofNullable(null));
// when, then
assertThatThrownBy(() -> postService.update(1L, createPostUpdateRequest("title", "content", 1234L, List.of(), List.of())))
.isInstanceOf(PostNotFoundException.class);
}
사용해오던 테스트 예제와 동일합니다.
업데이트된 이미지 결과에 따른 FileService의 행위도 검증해주고, 없는 게시글을 수정하는 상황도 테스트해주었습니다.
이제 controller.post.PostController에 게시글 수정 API를 추가해봅시다.
@ApiOperation(value = "게시글 수정", notes = "게시글을 수정한다.")
@PutMapping("/api/posts/{id}")
@ResponseStatus(HttpStatus.OK)
public Response update(
@ApiParam(value = "게시글 id", required = true) @PathVariable Long id,
@Valid @ModelAttribute PostUpdateRequest req) {
return Response.success(postService.update(id, req));
}
수정할 게시글의 id와 mutlipart/form-data 형태로 수정되어야할 데이터를 전달받게 됩니다.
이에 대한 Security 설정도 해주겠습니다.
config.security.SecurityConfig에 다음과 같이 추가해줍니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and()
.authorizeRequests()
...
.antMatchers(HttpMethod.POST, "/api/posts").authenticated()
.antMatchers(HttpMethod.PUT, "/api/posts/{id}").access("@postGuard.check(#id)")
.antMatchers(HttpMethod.DELETE, "/api/posts/{id}").access("@postGuard.check(#id)")
...
게시글 수정은, 작성자 본인 또는 관리자일 경우에만 수행할 수 있어야합니다.
지난 시간에 게시글 삭제를 처리하면서 작성했던 PostGuard를 그대로 적용하겠습니다.
이제 컨트롤러도 테스트해주겠습니다.
PostControllerTest, PostControllerAdviceTest, PostControllerIntegrationTest에 다음 테스트들을 추가해줍니다.
// PostControllerTest.java
@Test
void updateTest() throws Exception{
// given
ArgumentCaptor<PostUpdateRequest> postUpdateRequestArgumentCaptor = ArgumentCaptor.forClass(PostUpdateRequest.class);
List<MultipartFile> addedImages = List.of(
new MockMultipartFile("test1", "test1.PNG", MediaType.IMAGE_PNG_VALUE, "test1".getBytes()),
new MockMultipartFile("test2", "test2.PNG", MediaType.IMAGE_PNG_VALUE, "test2".getBytes())
);
List<Long> deletedImages = List.of(1L, 2L);
PostUpdateRequest req = createPostUpdateRequest("title", "content", 1234L, addedImages, deletedImages);
// when, then
mockMvc.perform(
multipart("/api/posts/{id}", 1L)
.file("addedImages", addedImages.get(0).getBytes())
.file("addedImages", addedImages.get(1).getBytes())
.param("deletedImages", String.valueOf(deletedImages.get(0)), String.valueOf(deletedImages.get(1)))
.param("title", req.getTitle())
.param("content", req.getContent())
.param("price", String.valueOf(req.getPrice()))
.with(requestPostProcessor -> {
requestPostProcessor.setMethod("PUT");
return requestPostProcessor;
})
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk());
verify(postService).update(anyLong(), postUpdateRequestArgumentCaptor.capture());
PostUpdateRequest capturedRequest = postUpdateRequestArgumentCaptor.getValue();
List<MultipartFile> capturedAddedImages = capturedRequest.getAddedImages();
assertThat(capturedAddedImages.size()).isEqualTo(2);
List<Long> capturedDeletedImages = capturedRequest.getDeletedImages();
assertThat(capturedDeletedImages.size()).isEqualTo(2);
assertThat(capturedDeletedImages).contains(deletedImages.get(0), deletedImages.get(1));
}
// PostControllerAdviceTest.java
@Test
void updateExceptionByPostNotFoundTest() throws Exception{
// given
given(postService.update(anyLong(), any())).willThrow(PostNotFoundException.class);
// when, then
mockMvc.perform(
multipart("/api/posts/{id}", 1L)
.param("title", "title")
.param("content", "content")
.param("price", "1234")
.with(requestPostProcessor -> {
requestPostProcessor.setMethod("PUT");
return requestPostProcessor;
})
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(-1012));
}
// PostControllerIntegrationTest.java
@Test
void updateByResourceOwnerTest() throws Exception {
// given
SignInResponse signInRes = signService.signIn(createSignInRequest(member1.getEmail(), initDB.getPassword()));
Post post = postRepository.save(createPost(member1, category));
String updatedTitle = "updatedTitle";
String updatedContent = "updatedContent";
Long updatedPrice = 1234L;
// when, then
mockMvc.perform(
multipart("/api/posts/{id}", post.getId())
.param("title", updatedTitle)
.param("content", updatedContent)
.param("price", String.valueOf(updatedPrice))
.with(requestPostProcessor -> {
requestPostProcessor.setMethod("PUT");
return requestPostProcessor;
})
.contentType(MediaType.MULTIPART_FORM_DATA)
.header("Authorization", signInRes.getAccessToken()))
.andExpect(status().isOk());
Post updatedPost = postRepository.findById(post.getId()).orElseThrow(PostNotFoundException::new);
assertThat(updatedPost.getTitle()).isEqualTo(updatedTitle);
assertThat(updatedPost.getContent()).isEqualTo(updatedContent);
assertThat(updatedPost.getPrice()).isEqualTo(updatedPrice);
}
@Test
void updateByAdminTest() throws Exception {
// given
SignInResponse adminSignInRes = signService.signIn(createSignInRequest(admin.getEmail(), initDB.getPassword()));
Post post = postRepository.save(createPost(member1, category));
String updatedTitle = "updatedTitle";
String updatedContent = "updatedContent";
Long updatedPrice = 1234L;
// when, then
mockMvc.perform(
multipart("/api/posts/{id}", post.getId())
.param("title", updatedTitle)
.param("content", updatedContent)
.param("price", String.valueOf(updatedPrice))
.with(requestPostProcessor -> {
requestPostProcessor.setMethod("PUT");
return requestPostProcessor;
})
.contentType(MediaType.MULTIPART_FORM_DATA)
.header("Authorization", adminSignInRes.getAccessToken()))
.andExpect(status().isOk());
Post updatedPost = postRepository.findById(post.getId()).orElseThrow(PostNotFoundException::new);
assertThat(updatedPost.getTitle()).isEqualTo(updatedTitle);
assertThat(updatedPost.getContent()).isEqualTo(updatedContent);
assertThat(updatedPost.getPrice()).isEqualTo(updatedPrice);
}
@Test
void updateUnauthorizedByNoneTokenTest() throws Exception {
// given
Post post = postRepository.save(createPost(member1, category));
String updatedTitle = "updatedTitle";
String updatedContent = "updatedContent";
Long updatedPrice = 1234L;
// when, then
mockMvc.perform(
multipart("/api/posts/{id}", post.getId())
.param("title", updatedTitle)
.param("content", updatedContent)
.param("price", String.valueOf(updatedPrice))
.with(requestPostProcessor -> {
requestPostProcessor.setMethod("PUT");
return requestPostProcessor;
})
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/exception/entry-point"));
}
@Test
void updateAccessDeniedByNotResourceOwnerTest() throws Exception {
// given
SignInResponse notOwnerSignInRes = signService.signIn(createSignInRequest(member2.getEmail(), initDB.getPassword()));
Post post = postRepository.save(createPost(member1, category));
String updatedTitle = "updatedTitle";
String updatedContent = "updatedContent";
Long updatedPrice = 1234L;
// when, then
mockMvc.perform(
multipart("/api/posts/{id}", post.getId())
.param("title", updatedTitle)
.param("content", updatedContent)
.param("price", String.valueOf(updatedPrice))
.with(requestPostProcessor -> {
requestPostProcessor.setMethod("PUT");
return requestPostProcessor;
})
.contentType(MediaType.MULTIPART_FORM_DATA)
.header("Authorization", notOwnerSignInRes.getAccessToken()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/exception/access-denied"));
}
익숙한 테스트이므로 자세한 설명은 생략하겠습니다.
테스트도 깔끔히 통과됐으니, 포스트맨으로 한 번 직접 테스트해보겠습니다.
먼저 게시글을 생성해줍니다.
1번 게시글이 생성되었습니다.
생성 결과를 조회해봅시다.
GET /api/posts/1
{
"success": true,
"code": 0,
"result": {
"data": {
"id": 1,
"title": "제목 입니다 ~",
"content": "내용 입니다 ~",
"price": 1000,
"member": {
"id": 2,
"email": "member1@member.com",
"username": "member1",
"nickname": "member1"
},
"images": [
{
"id": 1,
"originName": "1.PNG",
"uniqueName": "3712a35c-6580-4908-b2a9-df5e65f84224.PNG"
},
{
"id": 2,
"originName": "2.PNG",
"uniqueName": "089c4aff-158c-47f6-b7ff-734d8b6844ef.PNG"
}
]
}
}
}
id가 1번과 2번인 이미지가 생성되었습니다.
이제 수정 요청을 전송해보겠습니다.
1번 이미지는 제거하고, 3.PNG를 새롭게 추가하였습니다.
저장소도 업데이트되어있습니다.
조회 결과는 다음과 같습니다.
GET /api/posts/1
{
"success": true,
"code": 0,
"result": {
"data": {
"id": 1,
"title": "수정된 제목 입니다 ~",
"content": "수정된 내용 입니다 ~",
"price": 1234,
"member": {
"id": 2,
"email": "member1@member.com",
"username": "member1",
"nickname": "member1"
},
"images": [
{
"id": 2,
"originName": "2.PNG",
"uniqueName": "089c4aff-158c-47f6-b7ff-734d8b6844ef.PNG"
},
{
"id": 3,
"originName": "3.PNG",
"uniqueName": "50e696d3-8712-4377-b171-2fb828d4e7cb.PNG"
}
]
}
}
}
수정 내역이 정상적으로 반영되었습니다.
이번 시간에는 게시글 수정 내역을 구현해보았습니다.
다음 시간에는 게시글 목록 조회를 구현해보겠습니다.
* 질문 및 피드백은 환영입니다.
* 전체 소스코드에서는 여기에서 확인해볼 수 있습니다.
'Spring > 게시판 만들기' 카테고리의 다른 글
스프링부트 게시판 API 서버 만들기 (26) - 계층형 댓글 - 1 (6) | 2021.12.19 |
---|---|
스프링부트 게시판 API 서버 만들기 (25) - 게시글 목록 조회 - 페이징 및 검색 조건 (0) | 2021.12.15 |
스프링부트 게시판 API 서버 만들기 (23) - 게시글 - 삭제 (0) | 2021.12.13 |
스프링부트 게시판 API 서버 만들기 (22) - 게시글 - 조회 (4) | 2021.12.12 |
스프링부트 게시판 API 서버 만들기 (21) - 게시글 - 생성 (4) | 2021.12.12 |