Skip to content

Commit

Permalink
Merge pull request #113 from Seasoning-Today/feat/#112
Browse files Browse the repository at this point in the history
회원가입 시 자동으로 공식 계정 친구 추가
  • Loading branch information
csct3434 authored Apr 12, 2024
2 parents 6fd9f32 + d93d1e6 commit ba07849
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 5 deletions.
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ dependencies {
// logback-awslogs-appender
implementation "ca.pjer:logback-awslogs-appender:1.6.0"

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation ('org.springframework.boot:spring-boot-starter-test')
{
exclude group: 'org.mockito', module :'mockito-core'
}
testImplementation 'org.mockito:mockito-inline:5.2.0'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.rest-assured:rest-assured'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package today.seasoning.seasoning.user.event;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import today.seasoning.seasoning.user.domain.User;

@Getter
@RequiredArgsConstructor
public class SignUpEvent {

private final User signUpUser;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package today.seasoning.seasoning.user.event;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import today.seasoning.seasoning.friendship.domain.Friendship;
import today.seasoning.seasoning.friendship.domain.FriendshipRepository;
import today.seasoning.seasoning.user.domain.User;
import today.seasoning.seasoning.user.domain.UserRepository;

@Component
@RequiredArgsConstructor
public class SignUpEventHandler {

@Value("${OFFICIAL_ACCOUNT_USER_ID}")
private Long officialAccountUserId;

private final UserRepository userRepository;
private final FriendshipRepository friendshipRepository;

@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void addOfficialAccountFriend(SignUpEvent event) {
User signUpUser = event.getSignUpUser();
User officialUser = userRepository.findByIdOrElseThrow(officialAccountUserId);
friendshipRepository.save(new Friendship(signUpUser, officialUser));
friendshipRepository.save(new Friendship(officialUser, signUpUser));
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package today.seasoning.seasoning.user.service.kakao;

import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import today.seasoning.seasoning.common.enums.LoginType;
Expand All @@ -15,15 +15,17 @@
import today.seasoning.seasoning.user.dto.LoginResult;
import today.seasoning.seasoning.user.dto.SocialUserProfileDto;

import java.util.Optional;
import today.seasoning.seasoning.user.event.SignUpEvent;

@Slf4j
@Service
@RequiredArgsConstructor
public class KakaoLoginService {

private static final LoginType KAKAO_LOGIN_TYPE = LoginType.KAKAO;

private final ExchangeKakaoAccessToken exchangeKakaoAccessToken;
private final FetchKakaoUserProfile fetchKakaoUserProfile;
private final ApplicationEventPublisher eventPublisher;
private final UserRepository userRepository;

@Transactional
Expand All @@ -44,10 +46,11 @@ public LoginResult handleKakaoLogin(String authorizationCode) {
}

private LoginInfo handleLogin(SocialUserProfileDto userProfile) {
Optional<User> foundUser = userRepository.find(userProfile.getEmail(), KAKAO_LOGIN_TYPE);
Optional<User> foundUser = userRepository.find(userProfile.getEmail(), LoginType.KAKAO);

if (foundUser.isEmpty()) {
User user = userRepository.save(userProfile.toEntity(LoginType.KAKAO));
eventPublisher.publishEvent(new SignUpEvent(user));
return new LoginInfo(user, true);
}
return new LoginInfo(foundUser.get(), false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package today.seasoning.seasoning.user.event;

import org.assertj.core.api.SoftAssertions;
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.support.TransactionTemplate;
import today.seasoning.seasoning.BaseIntegrationTest;
import today.seasoning.seasoning.common.enums.LoginType;
import today.seasoning.seasoning.friendship.domain.FriendshipRepository;
import today.seasoning.seasoning.user.domain.User;
import today.seasoning.seasoning.user.domain.UserRepository;

@DisplayName("회원가입 이벤트 핸들러 통합 테스트")
@Sql(scripts = "classpath:data/insert_official_account_user.sql")
class SignUpEventHandlerIntegrationTest extends BaseIntegrationTest {

@Autowired
UserRepository userRepository;

@Autowired
TransactionTemplate transactionTemplate;

@Autowired
FriendshipRepository friendshipRepository;

@Autowired
ApplicationEventPublisher applicationEventPublisher;

@InjectSoftAssertions
SoftAssertions softAssertions;

@Value("${OFFICIAL_ACCOUNT_USER_ID}")
private Long officialAccountUserId;

private User user;

@BeforeEach
void registerUser() {
user = userRepository.save(new User("nickname0", "https://test.org/user0.jpg", "user0@email.com", LoginType.KAKAO));
}

@Test
@DisplayName("회원가입 이벤트 발생 시, 공식 계정을 신규 회원의 친구로 등록한다")
void test1() {
//given
SignUpEvent signUpEvent = new SignUpEvent(user);

//when
transactionTemplate.executeWithoutResult(status -> applicationEventPublisher.publishEvent(signUpEvent));

//then
softAssertions.assertThat(friendshipRepository.count()).isEqualTo(2);
softAssertions.assertThat(friendshipRepository.existsByUserIdAndFriendId(officialAccountUserId, user.getId())).isTrue();
softAssertions.assertThat(friendshipRepository.existsByUserIdAndFriendId(user.getId(), officialAccountUserId)).isTrue();
}

@Test
@DisplayName("회원가입이 실패한 경우, 발행된 회원가입 이벤트는 처리되지 않는다")
void test2() {
//given
SignUpEvent signUpEvent = new SignUpEvent(user);

//when
transactionTemplate.executeWithoutResult(transactionStatus -> {
applicationEventPublisher.publishEvent(signUpEvent);
transactionStatus.setRollbackOnly();
});

//then
softAssertions.assertThat(friendshipRepository.count()).isEqualTo(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package today.seasoning.seasoning.user.service.kakao;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mockStatic;
import static org.mockito.BDDMockito.verify;
import static org.mockito.Mockito.never;
import static today.seasoning.seasoning.common.enums.LoginType.KAKAO;

import java.util.Optional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import today.seasoning.seasoning.common.util.JwtUtil;
import today.seasoning.seasoning.user.domain.User;
import today.seasoning.seasoning.user.domain.UserRepository;
import today.seasoning.seasoning.user.dto.LoginResult;
import today.seasoning.seasoning.user.dto.SocialUserProfileDto;
import today.seasoning.seasoning.user.event.SignUpEvent;

@DisplayName("카카오 로그인 단위 테스트")
@ExtendWith(MockitoExtension.class)
class KakaoLoginServiceTest {

@Mock
private ExchangeKakaoAccessToken exchangeKakaoAccessToken;
@Mock
private FetchKakaoUserProfile fetchKakaoUserProfile;
@Mock
private ApplicationEventPublisher eventPublisher;
@Mock
private UserRepository userRepository;
@InjectMocks
KakaoLoginService kakaoLoginService;

private static MockedStatic<JwtUtil> jwtUtil;

@BeforeAll
static void beforeAll() {
jwtUtil = mockStatic(JwtUtil.class);
}

@AfterAll
static void afterAll() {
jwtUtil.close();
}

@Test
@DisplayName("비회원 로그인")
void test() {
//given
SocialUserProfileDto newUserProfile = new SocialUserProfileDto("user", "user@test.org", "https://test.org/image.jpg");
User newUser = newUserProfile.toEntity(KAKAO);

given(fetchKakaoUserProfile.doFetch(any())).willReturn(newUserProfile);
// 가입된 회원이 아닌 경우
given(userRepository.find(anyString(), any())).willReturn(Optional.empty());
given(userRepository.save(any(User.class))).willReturn(newUser);

//when: 카카오 로그인 시
LoginResult loginResult = kakaoLoginService.handleKakaoLogin("pseudo-authorization-code");

//then: 사용자를 신규 회원으로 등록해야 한다
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
User savedUser = userCaptor.getValue();

//then 1: 등록된 회원의 정보는 프로필 정보와 일치해야 한다
assertThat(savedUser.getLoginType()).isEqualTo(KAKAO);
assertThat(savedUser.getEmail()).isEqualTo(newUserProfile.getEmail());
assertThat(savedUser.getNickname()).isEqualTo(newUserProfile.getNickname());
assertThat(savedUser.getProfileImageUrl()).isEqualTo(newUserProfile.getProfileImageUrl());

//then 2: 등록된 회원에 대한 회원가입 이벤트가 발행되어야 한다
ArgumentCaptor<SignUpEvent> publishedEventCaptor = ArgumentCaptor.forClass(SignUpEvent.class);
verify(eventPublisher).publishEvent(publishedEventCaptor.capture());
SignUpEvent publishedEvent = publishedEventCaptor.getValue();

assertThat(publishedEvent.getSignUpUser()).isEqualTo(newUser);

//then 3: LoginResult.firstLogin의 값은 true이어야 한다
assertThat(loginResult.isFirstLogin()).isTrue();
}

@Test
@DisplayName("회원 로그인")
void test2() {
//given
SocialUserProfileDto userProfile = new SocialUserProfileDto("user", "user@test.org", "https://test.org/image.jpg");
User user = userProfile.toEntity(KAKAO);

given(fetchKakaoUserProfile.doFetch(any())).willReturn(userProfile);
// 가입된 회원의 경우
given(userRepository.find(anyString(), any())).willReturn(Optional.of(user));

//when: 카카오 로그인 시
LoginResult loginResult = kakaoLoginService.handleKakaoLogin("pseudo-authorization-code");

//then 1: 등록된 회원에 대한 토큰을 발행해야 한다
jwtUtil.verify(() -> JwtUtil.createToken(user.getId()));

//then 2: 회원가입 이벤트는 발행되지 않아야 한다
verify(eventPublisher, never()).publishEvent(any(SignUpEvent.class));

//then 3: LoginResult.firstLogin의 값은 false이어야 한다
assertThat(loginResult.isFirstLogin()).isFalse();
}

}
2 changes: 2 additions & 0 deletions src/test/resources/data/insert_official_account_user.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
INSERT INTO user (id, account_id, email, login_type, nickname, role)
VALUES (1, 'seasoning', 'seasoning@kakao.com', 'KAKAO', '시즈닝', 'USER');

0 comments on commit ba07849

Please sign in to comment.