✨개요
🏃 목표
📢 포스트 상세 조회, 수정 기능을 구현하자.
📢 요구사항
- 포스트 상세
- 회원, 비회원 모두 볼 수 있다.
- GET /posts/{postsId}
- id, 제목, 내용, 작성자, 작성날짜, 수정날짜
- 리턴 (JSON 형식)
{
"resultCode":"SUCCESS",
"result":{
"id" : 1,
"title" : "title1",
"body" : "body",
"userName" : "user1",
"createdAt" : yyyy-mm-dd hh:mm:ss,
"lastModifiedAt" : yyyy-mm-dd hh:mm:ss
}
}
- 포스트 수정
- ADMIN회원이나 글을 작성한 일반회원이 글에 대한 수정과 삭제를 할 수 있다.
- PUT /posts/{id}
- 입력폼 (JSON 형식)
{
"title" : "modified title",
"body" : "modified body"
}
- 리턴 (JSON 형식)
{
"resultCode":"SUCCESS",
"result":{
"message":"포스트 수정 완료",
"postId":0
}
}
✅ TO-DO
- 포스트 상세조회 컨트롤러, 서비스 테스트 작성
- 포스트 상세조회 컨트롤러 구현
- 포스트 상세조회 서비스 구현
- SecurityConfig 수정
- 포스트 수정 컨트롤러, 서비스 테스트 작성
- 포스트 수정 컨트롤러 구현
- 포스트 수정 서비스 구현
🔧 구현
포스트 상세조회 컨트롤러, 서비스 테스트 작성
컨트롤러 테스트 메서드 추가
<@Test
@DisplayName("포스트 상세 조회 성공")
@WithMockUser
void post_get_detail_SUCCESS() throws Exception {
String title = "테스트";
String body = "테스트입니다.";
when(postService.getPost(any()))
.thenReturn(postGetResponse);
mockMvc.perform(get("/api/v1/posts/1")
.with(csrf()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.result.id").exists())
.andExpect(jsonPath("$.result.title").exists())
.andExpect(jsonPath("$.result.body").exists())
.andExpect(jsonPath("$.result.userName").exists());
}
서비스 테스트 메서드 추가
@Test
@DisplayName("포스트 상세조회 성공")
void post_get_detail_SUCCESS() {
when(postRepository.findById(any()))
.thenReturn(Optional.of(post));
PostGetResponse postGetResponse = postService.getPost(post.getId());
assertEquals(postGetResponse.getUserName(),post.getUser().getUserName());
}
포스트 상세조회 컨트롤러 구현
PostController 메서드 추가
@GetMapping("/{id}")
public ResponseEntity<Response> getPost(@PathVariable Long id) {
PostGetResponse postGetResponse = postService.getPost(id);
return ResponseEntity.ok().body(Response.of("SUCCESS", postGetResponse));
}
PostGetResponse DTO 구현
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Setter
@Getter
public class PostGetResponse {
private Long id;
private String title;
private String body;
private String userName;
private LocalDateTime createdAt;
private LocalDateTime lastModifiedAt;
}
포스트 상세조회 서비스 구현
PostService 메서드 추가
public PostGetResponse getPost(Long id) {
Post findPost = postRepository.findById(id).orElseThrow(() -> {
throw new AppException(ErrorCode.POST_NOT_FOUND,ErrorCode.POST_NOT_FOUND.getMessage());
});
PostGetResponse postGetResponse = findPost.toResponse();
return postGetResponse;
}
Post 메서드 추가
public PostGetResponse toResponse(){
return PostGetResponse.builder()
.id(this.id)
.userName(this.user.getUserName())
.title(this.title)
.body(this.body)
.createdAt(getCreatedAt())
.lastModifiedAt(getModifiedAt())
.build();
}
SecurityConfig 수정
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationManager authenticationManager;
private final JwtFilter jwtFilter;
private String[] PERMIT_URL = {
"/api/v1/hello",
"/api/v1/users/**"
};
private String[] SWAGGER = {
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/v3/api-docs/**",
"/swagger-ui/**"
};
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers(SWAGGER).permitAll()
.antMatchers(PERMIT_URL).permitAll()
.antMatchers(HttpMethod.GET).permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(authenticationManager)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
포스트 수정 컨트롤러, 서비스 테스트 작성
포스트 수정 컨트롤러 테스트 메서드 작성
@Test
@DisplayName("포스트 수정 성공")
@WithMockUser
void post_modify_SUCCESS() throws Exception {
String title = "테스트";
String body = "테스트입니다.";
when(postService.modify(any(),any(),any(),any()))
.thenReturn(new PostResponse(title, 1l));
mockMvc.perform(put("/api/v1/posts/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new PostRequest(title, body))))
.andDo(print())
.andExpect(status().isOk());
}
@Test
@DisplayName("포스트 수정 실패1_인증")
@WithAnonymousUser
void post_modify_FAILED_authentication() throws Exception {
String title = "테스트";
String body = "테스트입니다.";
when(postService.modify(any(),any(),any(),any()))
.thenThrow(new AppException(ErrorCode.INVALID_PERMISSION,ErrorCode.INVALID_PERMISSION.getMessage()));
mockMvc.perform(put("/api/v1/posts/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new PostRequest(title, body))))
.andDo(print())
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("포스트 수정 실패2_작성자 불일치")
@WithMockUser
void post_modify_FAILED_different() throws Exception {
String title = "테스트";
String body = "테스트입니다.";
when(postService.modify(any(),any(),any(),any()))
.thenThrow(new AppException(ErrorCode.INVALID_PERMISSION,ErrorCode.INVALID_PERMISSION.getMessage()));
mockMvc.perform(put("/api/v1/posts/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new PostRequest(title, body))))
.andDo(print())
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("포스트 수정 실패3_데이터베이스 에러")
@WithMockUser
void post_modify_FAILED_db() throws Exception {
String title = "테스트";
String body = "테스트입니다.";
when(postService.modify(any(),any(),any(),any()))
.thenThrow(new AppException(ErrorCode.DATABASE_ERROR,ErrorCode.DATABASE_ERROR.getMessage()));
mockMvc.perform(put("/api/v1/posts/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new PostRequest(title, body))))
.andDo(print())
.andExpect(status().isInternalServerError());
}
포스트 수정 서비스 테스트 메서드 작성
@Test
@DisplayName("포스트 수정 성공")
void post_modify_SUCCESS() {
when(postRepository.findById(any()))
.thenReturn(Optional.of(post));
when(userRepository.findByUserName(any()))
.thenReturn(Optional.of(user));
assertDoesNotThrow(() -> postService.modify(user.getUserName(), post.getId(), modifyPost.getTitle(), modifyPost.getBody()));
}
@Test
@DisplayName("포스트 수정 실패1_포스트가 존재하지 않는 경우")
void post_modify_FAILED_not_found_post() {
when(postRepository.findById(any()))
.thenReturn(Optional.empty());
when(userRepository.findByUserName(any()))
.thenReturn(Optional.of(user));
AppException exception = assertThrows(AppException.class, () -> postService.modify(user.getUserName(), post.getId(), modifyPost.getTitle(), modifyPost.getBody()));
assertEquals(ErrorCode.POST_NOT_FOUND, exception.getErrorCode());
}
@Test
@DisplayName("포스트 수정 실패2_포스트 작성자와 유저가 다른 경우")
void post_modify_FAILED_different() {
when(postRepository.findById(any()))
.thenReturn(Optional.of(post));
when(userRepository.findByUserName(any()))
.thenReturn(Optional.of(user2));
AppException exception = assertThrows(AppException.class, () -> postService.modify(user.getUserName(), post.getId(), modifyPost.getTitle(), modifyPost.getBody()));
assertEquals(ErrorCode.INVALID_PERMISSION, exception.getErrorCode());
}
@Test
@DisplayName("포스트 수정 실패3_유저가 존재하지 않는 경우")
void post_modify_FAILED_not_found_userName() {
when(postRepository.findById(any()))
.thenReturn(Optional.of(post));
when(userRepository.findByUserName(any()))
.thenReturn(Optional.empty());
AppException exception = assertThrows(AppException.class, () -> postService.modify(user.getUserName(), post.getId(), modifyPost.getTitle(), modifyPost.getBody()));
assertEquals(ErrorCode.INVALID_PERMISSION, exception.getErrorCode());
}
포스트 수정 컨트롤러 구현
PostController 메서드 추가
@PutMapping("/{id}")
public ResponseEntity<Response> modifyPost(@RequestBody PostRequest postRequest, @PathVariable Long id, Authentication authentication) {
String userName = null;
try {
userName = authentication.getName();
} catch (Exception e) {
throw new AppException(ErrorCode.INVALID_TOKEN, ErrorCode.INVALID_TOKEN.getMessage());
}
PostResponse postResponse = postService.modify(userName, id, postRequest.getTitle(), postRequest.getBody());
return ResponseEntity.ok().body(Response.of("SUCCESS", postResponse));
}
PostDTO
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class PostRequest {
private String title;
private String body;
}
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class PostResponse {
private String message;
private Long postId;
public static PostResponse of(String message, Long postId) {
return PostResponse.builder()
.message(message)
.postId(postId)
.build();
}
}
포스트 수정 서비스 구현
public PostResponse modify(String userName, Long id, String title, String body) {
Post modifyPost = postRepository.findById(id).orElseThrow(() -> {
throw new AppException(ErrorCode.POST_NOT_FOUND, ErrorCode.POST_NOT_FOUND.getMessage());
});
User user = userRepository.findByUserName(userName).orElseThrow(() -> {
throw new AppException(ErrorCode.INVALID_PERMISSION, ErrorCode.INVALID_PERMISSION.getMessage());
});
if (!modifyPost.getUser().getUserName().equals(user.getUserName())) {
throw new AppException(ErrorCode.INVALID_PERMISSION, ErrorCode.INVALID_PERMISSION.getMessage());
}
modifyPost.setTitle(title);
modifyPost.setBody(body);
postRepository.saveAndFlush(modifyPost);
PostResponse postResponse = PostResponse.of("포스트 수정 완료", modifyPost.getId());
return postResponse;
}
🌉 회고
- 아직 섬세하진 못하지만 TDD에 대해서 조금씩 익숙해지기 시작했다.
- 하지만 시큐리티 부분은 제대로 테스트가 이루어지지 않는 점에서 보완이 필요할 것 같다.