Skip to content

Commit

Permalink
release: 0.4.2 (#205)
Browse files Browse the repository at this point in the history
* test: test 명 완성

* refactor: Instant를 모두 LocalDateTime으로 변경 (#200)

* fix: 알림이 가지 않는 버그 수정

* �test: 인증 단위 테스트 구현 (#202)

* refactor: JwtAuthenticationFilter 불필요한 로직 제거 (#198)

* refactor: SecurityValue 패키지 이동 (#198)

* test: 인증 관련 단위 테스트 구현 (#198)

* test: 인증 관련 단위 테스트 구현 (#198)

* test: JwtAuthenticationFilterTest 리팩토링 (#198)

* feat: 로그인한 유저가 북마크한 모임 조회 (#204)

* feat: 로그인한 유저가 북마크한 모임 조회

* test: 테스트 코드에 로그인 로직 추가

---------

Co-authored-by: choidongkuen <[email protected]>
Co-authored-by: devxb <[email protected]>
Co-authored-by: xb205 <[email protected]>
  • Loading branch information
4 people authored Feb 12, 2024
1 parent 914b670 commit b369908
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 30 deletions.
1 change: 0 additions & 1 deletion src/main/java/net/teumteum/alert/app/AlertHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public class AlertHandler {
@Async(ALERT_EXECUTOR)
@EventListener(BeforeMeetingAlerted.class)
public void handleBeforeMeetingAlerts(BeforeMeetingAlerted alerted) {
System.out.println(">>> handleBeforeMeetingAlerts(" + alerted + " )");
userAlertService.findAllByUserId(alerted.userIds())
.stream()
.map(userAlert -> Pair.of(userAlert.getToken(),
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/net/teumteum/alert/domain/AlertService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package net.teumteum.alert.domain;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.ZoneId;
import lombok.RequiredArgsConstructor;
import net.teumteum.alert.domain.response.AlertsResponse;
import org.springframework.scheduling.annotation.Scheduled;
Expand Down Expand Up @@ -33,7 +33,8 @@ public AlertsResponse findAllByUserId(Long userId) {
@Transactional
@Scheduled(cron = EVERY_12AM)
public void deleteOneMonthBeforeAlert() {
var deleteTargets = alertRepository.findAllByCreatedAt(LocalDateTime.now().minus(1, ChronoUnit.MONTHS));
var deleteTargets = alertRepository.findAllByCreatedAt(
LocalDateTime.now(ZoneId.of("Asia/Seoul")).minusMonths(1));
alertRepository.deleteAllInBatch(deleteTargets);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ public class FcmAlertPublisher implements AlertPublisher {
@Async(FCM_ALERT_EXECUTOR)
public void publish(String token, Alert alert, Map<String, String> data) {
var message = buildMessage(token, alert, data);
System.out.println(">>> message" + message);
publishWithRetry(0, message, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

Expand All @@ -31,7 +30,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProperty jwtProperty;

@Override
protected void doFilterInternal(HttpServletRequest request,
public void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (request.getMethod().equals("OPTIONS")) {
Expand Down Expand Up @@ -73,10 +72,9 @@ private void saveUserAuthentication(User user) {

private String resolveTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(jwtProperty.getAccess().getHeader());
if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) {
if (token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) {
return token.substring(7);
}
setRequestAttribute(request, "요청에 대한 JWT 파싱 과정에서 문제가 발생했습니다.");
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ public PageDto<MeetingsResponse> getMeetingsByCondition(
@RequestParam(value = "topic", required = false) Topic topic,
@RequestParam(value = "meetingAreaStreet", required = false) String meetingAreaStreet,
@RequestParam(value = "participantUserId", required = false) Long participantUserId,
@RequestParam(value = "isBookmarked", required = false) Boolean isBookmarked,
@RequestParam(value = "searchWord", required = false) String searchWord) {

Long userId = securityService.getCurrentUserId();
return meetingService.getMeetingsBySpecification(pageable, topic, meetingAreaStreet, participantUserId,
searchWord, isOpen);
searchWord, isBookmarked, isOpen, userId);
}

@PutMapping("/{meetingId}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ public static Specification<Meeting> withParticipantUserId(Long participantUserI
participantUserId);
}

public static Specification<Meeting> withBookmarkedUserId(Long userId) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.join("bookmarkedUserIds"), userId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ public class MeetingAlertPublisher {
@Scheduled(cron = EVERY_ONE_MINUTES)
public void alertBeforeMeeting() {
var alertStart = LocalDateTime.now(ZoneId.of("Asia/Seoul")).plusMinutes(5).withNano(0).withSecond(0);
System.out.println(">>> alertStart = " + alertStart);
var alertEnd = alertStart.plusMinutes(1).withNano(0).withSecond(0);
System.out.println(">>> alertEnd = " + alertEnd);
var alertTargets = meetingRepository.findAlertMeetings(alertStart, alertEnd);
alertTargets.forEach(target -> System.out.println(">>> target id = " + target.getId()));
alertTargets.forEach(meeting -> eventPublisher.publishEvent(
new BeforeMeetingAlerted(meeting.getParticipantUserIds())
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ public void deleteMeeting(Long meetingId, Long userId) {
}

@Transactional(readOnly = true)
public PageDto<MeetingsResponse> getMeetingsBySpecification(Pageable pageable, Topic topic,
String meetingAreaStreet,
Long participantUserId, String searchWord, boolean isOpen) {
public PageDto<MeetingsResponse> getMeetingsBySpecification(Pageable pageable, Topic topic, String meetingAreaStreet,
Long participantUserId, String searchWord, Boolean isBookmarked, boolean isOpen, Long userId) {

Specification<Meeting> spec = MeetingSpecification.withIsOpen(isOpen);

Expand All @@ -110,6 +109,8 @@ public PageDto<MeetingsResponse> getMeetingsBySpecification(Pageable pageable, T
spec = MeetingSpecification.withSearchWordInTitle(searchWord)
.or(MeetingSpecification.withSearchWordInIntroduction(searchWord))
.and(MeetingSpecification.withIsOpen(isOpen));
} else if (Boolean.TRUE.equals(isBookmarked)) {
spec = spec.and(MeetingSpecification.withBookmarkedUserId(userId));
}

var meetings = meetingRepository.findAll(spec, pageable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ class Find_meeting_list_api {
@DisplayName("존재하는 topic이 주어지면 페이지 네이션을 적용해 미팅 목록을 최신순으로 응답한다.")
void Return_meeting_list_if_topic_and_page_nation_received() {
// given
var user = repository.saveAndGetUser();
securityContextSetting.set(user.getId());
var size = 2;
var openMeetingsByTopic = repository.saveAndGetOpenMeetingsByTopic(size, Topic.스터디);
var closeTopicMeetingsByTopic = repository.saveAndGetOpenMeetingsByTopic(size, Topic.고민_나누기);
Expand Down Expand Up @@ -169,6 +171,8 @@ void Return_meeting_list_if_topic_and_page_nation_received() {
@DisplayName("제목이나 설명에 존재하는 검색어가 주어지면 페이지 네이션을 적용해 미팅 목록을 최신순으로 응답한다.")
void Return_meeting_list_if_search_word_and_page_nation_received() {
// given
var user = repository.saveAndGetUser();
securityContextSetting.set(user.getId());
var size = 2;
var openMeetingsByTitle = repository.saveAndGetOpenMeetingsByTitle(size, "개발자 스터디");
var closeMeetingsByTitle = repository.saveAndGetCloseMeetingsByTitle(size, "개발자 스터디");
Expand Down Expand Up @@ -201,6 +205,8 @@ void Return_meeting_list_if_search_word_and_page_nation_received() {
@DisplayName("참여자 id가 주어지면 페이지 네이션을 적용해 미팅 목록을 최신순으로 응답한다.")
void Return_meeting_list_if_participant_user_id_and_page_nation_received() {
// given
var user = repository.saveAndGetUser();
securityContextSetting.set(user.getId());
var size = 2;
var openMeetingsByParticipantUserId = repository.saveAndGetOpenMeetingsByParticipantUserId(size, 2L);
var closeMeetingsByParticipantUserId = repository.saveAndGetCloseMeetingsByParticipantUserId(size, 2L);
Expand Down Expand Up @@ -229,6 +235,8 @@ void Return_meeting_list_if_participant_user_id_and_page_nation_received() {
@DisplayName("요청한 size와 page보다 더 많은 데이터가 존재하면, hasNext를 true로 응답한다.")
void Return_has_next_true_if_more_data_exists_than_requested_size_and_page() {
// given
var user = repository.saveAndGetUser();
securityContextSetting.set(user.getId());
var size = 10;
var openMeetingsByTopic = repository.saveAndGetOpenMeetingsByTopic(size, Topic.스터디);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package net.teumteum.unit.auth.controller;


import static net.teumteum.unit.auth.common.SecurityValue.INVALID_ACCESS_TOKEN;
import static net.teumteum.unit.auth.common.SecurityValue.INVALID_REFRESH_TOKEN;
import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN;
import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN;
import static net.teumteum.unit.common.SecurityValue.INVALID_ACCESS_TOKEN;
import static net.teumteum.unit.common.SecurityValue.INVALID_REFRESH_TOKEN;
import static net.teumteum.unit.common.SecurityValue.VALID_ACCESS_TOKEN;
import static net.teumteum.unit.common.SecurityValue.VALID_REFRESH_TOKEN;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package net.teumteum.unit.auth.service;

import static net.teumteum.core.security.Authenticated.네이버;
import static net.teumteum.unit.auth.common.SecurityValue.INVALID_ACCESS_TOKEN;
import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN;
import static net.teumteum.unit.common.SecurityValue.INVALID_ACCESS_TOKEN;
import static net.teumteum.unit.common.SecurityValue.VALID_REFRESH_TOKEN;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package net.teumteum.unit.auth.common;
package net.teumteum.unit.common;

public final class SecurityValue {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package net.teumteum.unit.core.security;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import net.teumteum.core.error.ErrorResponse;
import net.teumteum.core.security.filter.JwtAuthenticationEntryPoint;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.DelegatingServletOutputStream;
import org.springframework.security.core.AuthenticationException;

@ExtendWith(MockitoExtension.class)
@DisplayName("JwtAuthenticationEntryPoint 단위 테스트의")
public class JwtAuthenticationEntryPointTest {

private static final String ATTRIBUTE_NAME = "exception";
@Mock
private ObjectMapper objectMapper;
@Mock
private AuthenticationException authenticationException;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@InjectMocks
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Nested
@DisplayName("JwtAuthenticationFilter 에서 인증 예외가 발생시")
class When_authentication_error_occurs_from_filter {

@Test
@DisplayName("알맞은 예외 메시지와 관련 응답을 반환한다.")
void Return_error_response_with_message() throws IOException {
// given
var errorMessage = "Authentication Exception Occurred";
var outputStream = new ByteArrayOutputStream();

given(request.getAttribute(ATTRIBUTE_NAME)).willReturn(errorMessage);
given(response.getOutputStream()).willReturn(new DelegatingServletOutputStream(outputStream));

// when
jwtAuthenticationEntryPoint.commence(request, response, authenticationException);

// then
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
verify(objectMapper, times(1)).writeValue(any(OutputStream.class), any(ErrorResponse.class));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package net.teumteum.unit.core.security;

import static net.teumteum.unit.common.SecurityValue.INVALID_ACCESS_TOKEN;
import static net.teumteum.unit.common.SecurityValue.VALID_ACCESS_TOKEN;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import java.io.IOException;
import net.teumteum.auth.service.AuthService;
import net.teumteum.core.property.JwtProperty;
import net.teumteum.core.security.UserAuthentication;
import net.teumteum.core.security.filter.JwtAuthenticationFilter;
import net.teumteum.core.security.service.JwtService;
import net.teumteum.user.domain.UserFixture;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;

@ExtendWith(MockitoExtension.class)
@DisplayName("JwtAuthenticationFilter 단위 테스트의")
public class JwtAuthenticationFilterTest {

private static final String ATTRIBUTE_NAME = "exception";
@Mock
private JwtService jwtService;
@Mock
private AuthService authService;
@Mock
private JwtProperty jwtProperty;
@Mock
private JwtProperty.Access access;
@Mock
private FilterChain filterChain;
@InjectMocks
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Nested
@DisplayName("API 요청시 JWT 파싱 및 회원 조회 로직은")
class Api_request_with_valid_jwt_unit {

@BeforeEach
@AfterEach
void clearSecurityContextHolder() {
SecurityContextHolder.clearContext();
}

@Test
@DisplayName("유효한 JWT 인 경우, JWT 을 파싱하고 성공적으로 UserAuthentication 을 SecurityContext 에 저장한다.")
void Parsing_jwt_and_save_user_in_security_context() throws ServletException, IOException {
// given
var request = new MockHttpServletRequest();
var response = new MockHttpServletResponse();

given(jwtProperty.getAccess()).willReturn(access);
given(jwtProperty.getAccess().getHeader()).willReturn("Authorization");
given(jwtProperty.getBearer()).willReturn("Bearer");

request.addHeader(jwtProperty.getAccess().getHeader(),
jwtProperty.getBearer() + " " + VALID_ACCESS_TOKEN);

var user = UserFixture.getIdUser();

given(jwtService.validateToken(anyString())).willReturn(true);
given(authService.findUserByAccessToken(anyString())).willReturn(user);

// when
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);

// then
var authentication = SecurityContextHolder.getContext().getAuthentication();
assertThat(authentication).isInstanceOf(UserAuthentication.class);
}

@Test
@DisplayName("유효하지 않은 JWT 와 함께 요청이 들어오면, 요청 처리를 중단하고 에러 메세지를 반환한다.")
void Return_error_when_jwt_is_invalid() throws ServletException, IOException {
// given
var request = new MockHttpServletRequest();
var response = new MockHttpServletResponse();

given(jwtProperty.getAccess()).willReturn(access);
given(jwtProperty.getAccess().getHeader()).willReturn("Authorization");
given(jwtProperty.getBearer()).willReturn("Bearer");

request.addHeader(jwtProperty.getAccess().getHeader(),
jwtProperty.getBearer() + " " + INVALID_ACCESS_TOKEN);

given(jwtService.validateToken(anyString())).willReturn(false);

// when
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);

// then
assertThat(request.getAttribute(ATTRIBUTE_NAME)).isEqualTo("요청에 대한 JWT 가 유효하지 않습니다.");
var authentication = SecurityContextHolder.getContext().getAuthentication();
assertThat(authentication).isNull();
verify(filterChain, times(1)).doFilter(request, response);
}

@Test
@DisplayName("JWT 가 존재하지 않는 경우, 요청 처리를 중단하고 에러 메세지를 반환한다.")
void Return_error_when_jwt_is_not_exist() throws ServletException, IOException {
// given
var request = new MockHttpServletRequest();
var response = new MockHttpServletResponse();

given(jwtProperty.getAccess()).willReturn(access);
given(jwtProperty.getAccess().getHeader()).willReturn("Authorization");
given(jwtProperty.getBearer()).willReturn("Bearer");

request.addHeader(jwtProperty.getAccess().getHeader(),
jwtProperty.getBearer() + " ");

// when
jwtAuthenticationFilter.doFilterInternal(request, response, filterChain);

// then
assertThat(request.getAttribute(ATTRIBUTE_NAME)).isEqualTo("요청에 대한 JWT 정보가 존재하지 않습니다.");
verify(jwtService, times(0)).validateToken(anyString());
}
}
}
Loading

0 comments on commit b369908

Please sign in to comment.