Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

인증 & 인가 관련 테스트 코드 리펙토링 #476

Merged
merged 20 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5e8529e
test: JwtDecoderTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 28, 2023
d04699a
test: JwtEncoderTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 28, 2023
979e1dc
test: 사용하지 않는 필드 제거와 final 제거
apptie Sep 28, 2023
440d2cd
test: KakaoOauth2TypeTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 28, 2023
1a7523d
test: KakaoUserInformationProviderTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 28, 2023
3533136
test: JpaBlackListTokenRepositoryTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 28, 2023
7932318
test: AuthenticationServiceFixture Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 28, 2023
870b4d3
test: AuthenticationUserServiceTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 29, 2023
99a9a2e
test: Fixture가 가지고 있던 테스트 관련 어노테이션을 기존 테스트 클래스로 변경
apptie Sep 29, 2023
1eeef52
test: BlackListTokenServiceTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 29, 2023
c641416
refactor: 로그인 메서드 네이밍 변경
apptie Sep 29, 2023
fbb1fd7
test: AuthenticationControllerTest Fixture 추가 및 테스트 케이스 리펙토링
apptie Sep 29, 2023
84b6e99
test: 누락된 테스트 케이스 추가
apptie Sep 29, 2023
357784d
test: AuthenticationServiceTest Fixture 및 테스트 케이스 수정
apptie Sep 29, 2023
524069c
test: AuthenticationControllerTest Fixture 및 테스트 케이스 수정
apptie Sep 29, 2023
737d88b
test: KakaoUserInformationProviderTest Fixture 및 테스트 케이스 수정
apptie Sep 29, 2023
d5d0221
test: JpaBlackListTokenRepositoryFixture Fixture 내용 변경
apptie Sep 29, 2023
ef0a0f5
test: AuthenticationServiceTest 관련 사용하지 않는 Fixture 삭제 및 필드 변경
apptie Sep 29, 2023
6d1d02b
test: KakaoUserInformationProviderTest 관련 테스트 대상을 Fixture에서 테스트 클래스로 변경
apptie Sep 30, 2023
ba5506e
ci: 브랜치 최신화
apptie Sep 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class AuthenticationController {
private final BlackListTokenService blackListTokenService;

@PostMapping("/login/{oauth2Type}")
public ResponseEntity<Object> validate(
public ResponseEntity<Object> login(
@PathVariable final Oauth2Type oauth2Type,
@RequestBody final LoginTokenRequest request
) {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,62 +1,35 @@
package com.ddang.ddang.authentication.application;

import static org.assertj.core.api.Assertions.assertThat;

import com.ddang.ddang.authentication.application.fixture.AuthenticationUserServiceFixture;
import com.ddang.ddang.configuration.IsolateDatabase;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import static org.assertj.core.api.Assertions.assertThat;

@IsolateDatabase
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
class AuthenticationUserServiceTest {

@Autowired
JpaUserRepository userRepository;

class AuthenticationUserServiceTest extends AuthenticationUserServiceFixture {

@Autowired
AuthenticationUserService authenticationUserService;

@Test
void 회원탈퇴한_회원의_id를_전달하면_참을_반환한다() {
// given
final User user = User.builder()
.name("회원")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12345")
.build();

user.withdrawal();
userRepository.save(user);

// when
final boolean actual = authenticationUserService.isWithdrawal(user.getId());
final boolean actual = authenticationUserService.isWithdrawal(탈퇴한_사용자.getId());

// then
assertThat(actual).isTrue();
}

@Test
void 회원탈퇴하지_않거나_회원가입하지_않은_회원의_id를_전달하면_거짓을_반환한다() {
// given
final User user = User.builder()
.name("회원")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12345")
.build();

userRepository.save(user);

// when
final boolean actual = authenticationUserService.isWithdrawal(user.getId());
final boolean actual = authenticationUserService.isWithdrawal(사용자.getId());

// then
assertThat(actual).isFalse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

import com.ddang.ddang.authentication.domain.TokenEncoder;
import com.ddang.ddang.authentication.application.fixture.BlackListTokenServiceFixture;
import com.ddang.ddang.authentication.domain.TokenType;
import com.ddang.ddang.authentication.domain.exception.EmptyTokenException;
import com.ddang.ddang.authentication.infrastructure.persistence.JpaBlackListTokenRepository;
import com.ddang.ddang.configuration.IsolateDatabase;
import java.time.LocalDateTime;
import java.util.Map;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
Expand All @@ -21,28 +18,15 @@
@IsolateDatabase
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
class BlackListTokenServiceTest {
class BlackListTokenServiceTest extends BlackListTokenServiceFixture {

@Autowired
BlackListTokenService blackListTokenService;

@Autowired
JpaBlackListTokenRepository blackListTokenRepository;

@Autowired
TokenEncoder tokenEncoder;

@Test
void 유효한_accessToken과_refreshToken을_전달하면_블랙리스트로_등록한다() {
// given
final Map<String, Object> privateClaims = Map.of("userId", 1L);
final String accessToken =
"Bearer " + tokenEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, privateClaims);
final String refreshToken =
"Bearer " + tokenEncoder.encode(LocalDateTime.now(), TokenType.REFRESH, privateClaims);

// when & then
assertDoesNotThrow(() -> blackListTokenService.registerBlackListToken(accessToken, refreshToken));
assertDoesNotThrow(() -> blackListTokenService.registerBlackListToken(유효한_액세스_토큰, 유효한_리프레시_토큰));
}

@ParameterizedTest
Expand All @@ -59,27 +43,17 @@ class BlackListTokenServiceTest {

@Test
void 블랙리스트로_등록된_토큰인지_확인할때_이미_블랙리스트로_등록된_토큰을_전달하면_참을_반환한다() {
// given
final Map<String, Object> privateClaims = Map.of("userId", 1L);
final String accessToken = tokenEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, privateClaims);
final String refreshToken = tokenEncoder.encode(LocalDateTime.now(), TokenType.REFRESH, privateClaims);

blackListTokenService.registerBlackListToken(accessToken, refreshToken);

// when
final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, accessToken);
final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, 만료된_액세스_토큰);

// then
assertThat(actual).isTrue();
}

@Test
void 블랙리스트로_등록된_토큰인지_확인할때_블랙리스트로_등록되지_않은_토큰을_전달하면_거짓을_반환한다() {
// given
final String invalidAccessToken = "invalidAccessToken";

// when
final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, invalidAccessToken);
final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, 유효한_액세스_토큰);

// then
assertThat(actual).isFalse();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package com.ddang.ddang.authentication.application.fixture;

import static org.mockito.Mockito.mock;

import com.ddang.ddang.authentication.application.AuthenticationService;
import com.ddang.ddang.authentication.application.BlackListTokenService;
import com.ddang.ddang.authentication.domain.Oauth2UserInformationProviderComposite;
import com.ddang.ddang.authentication.domain.TokenDecoder;
import com.ddang.ddang.authentication.domain.TokenEncoder;
import com.ddang.ddang.authentication.domain.TokenType;
import com.ddang.ddang.authentication.domain.dto.UserInformationDto;
import com.ddang.ddang.authentication.infrastructure.oauth2.OAuth2UserInformationProvider;
import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type;
import com.ddang.ddang.device.application.DeviceTokenService;
import com.ddang.ddang.device.domain.DeviceToken;
import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;

@SuppressWarnings("NonAsciiCharacters")
public class AuthenticationServiceFixture {

@MockBean
protected Oauth2UserInformationProviderComposite 소셜_회원_정보_제공자_묶음;

@MockBean
protected OAuth2UserInformationProvider 소셜_회원_정보_제공자;

protected AuthenticationService authenticationService;
protected AuthenticationService profileImageAuthenticationService;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필수

AuthenticationServiceTest에서는 AuthenticationService가 테스트하려는 객체이므로 픽스처 클래스보다는 테스트 클래스에 있는 것이 더 적절할 것 같습니다!

선택

서비스 테스트만 봤을 때 profileImageAuthenticationService라는 이름으로는 프로필 이미지를 찾을 수 없는 상황을 가정한 service라는 것을 파악하기 힘들었습니다.
네이밍이 좀 길어지긴 하겠지만 profileImageNotFoundAuthenticationService라던가 nullProfileImageAuthenticationService 라든가 ProfileImageErrorAuthenticationService 등등 좀 더 상황을 구체적으로 나타내는 네이밍이면 좋을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthenticationServiceTest에서는 AuthenticationService가 테스트하려는 객체이므로 픽스처 클래스보다는 테스트 클래스에 있는 것이 더 적절할 것 같습니다!

이동했습니다

profileImageAuthenticationService 네이밍 관련

profileImageNotFoundAuthenticationService로 변경하도록 하겠습니다

protected JpaProfileImageRepository 프로필_이미지_저장소 = mock(JpaProfileImageRepository.class);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선택

프로필 이미지가 없는 저장소는 어떤가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 이슈는 밑에 Service를 테스트 클래스로 이동시키면서 사라졌고 영어 이름을 사용하는 식으로 진행했습니다
생각해보면 Fixture는 현재 테스트에서 직접적으로 테스트하지는 않지만 연관관계 상 필요한 도메인 엔티티 등을 만드는 것이니 Service를 구현하는데 필요한 구성요소들을 Fixture로 만드는건 좀 이상하다 싶기는 하네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

계속 제가 Fixture로 뭘 쓸지 헷갈려하고 있는데 일단 스프링 빈으로 등록된건 모조리 제외하는 식으로 하는게 맞겠네요


protected Oauth2Type 지원하는_소셜_로그인 = Oauth2Type.KAKAO;
protected Oauth2Type 지원하지_않는_소셜_로그인 = Oauth2Type.KAKAO;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문

아래는 지원하지 않는 소셜 로그인인데 왜 동일한 걸까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선택

추가적으로 지원하는_소셜_로그인_타입 이런식으로 타입을 붙이면 어떨까요?

Copy link
Collaborator Author

@apptie apptie Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래는 지원하지 않는 소셜 로그인인데 왜 동일한 걸까요?

그 이유는 Oauth2Type 필드가 하나밖에 존재하지 않기 때문입니다...null로 표현하는것보다는 그냥 동일하게 표현하는게 낫다고 판단했습니다

추가적으로 지원하는_소셜_로그인_타입 이런식으로 타입을 붙이면 어떨까요?

이건...좋을 것 같네요 적용하겠습니다

Copy link
Member

@JJ503 JJ503 Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하지만, 현재 로직(findProvider())을 살펴본 결과 해당 오류가 발생하는 경우는 오로지 null인 경우 예외를 반환하는 것으로 보이는 데 아닐까요?
일단 해당 픽스처는 지원하는 소셜 로그인 타입과 지원하지 않는 소셜 로그인 타입이 동일해 의문을 갖게 만드는 듯합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이렇게 한 이유는 로직상 null이 올 수 없기 때문입니다
Converter에서 값이 없거나 존재하지 않으면 Controller까지 오지 못하기 때문입니다
api 호출 시의 문자열이 null일 수는 있지만 이로 인해 Oauth2TypeConverter에서 예외가 발생하므로 Oauth2Type이 null인 경우는 존재하지 않습니다
그래서 AuthenticationService에서 이를 고려할 필요는 없다고 생각합니다

현재 테스트 케이스에서 지원하지 않는 소셜 로그인 타입을 null로 주게 되면 NPE가 발생하게 됩니다
이 경우 테스트 케이스에서 검증하고자 하는 내용과는 다르며, NPE를 핸들링하는 예외 처리 로직을 추가하게 된다면 이미 다른 구간에서 핸들링하고 있는 경우를 테스트 케이스(Fixture) 하나로 인해 프로덕션 코드가 영향을 받게 되어 이는 테스트하는 목적과는 전혀 맞지 않다고 생각합니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그렇군요! provier에 대한 검증인데 제가 잘못 확인했네요 죄송합니다..!


protected String 유효한_소셜_로그인_토큰 = "Bearer accessToken";
protected String 유효하지_않은_소셜_로그인_토큰 = "accessToken";
protected String 만료된_소셜_로그인_토큰;

protected String 디바이스_토큰 = "deviceToken";

protected User 사용자;
protected User 탈퇴한_사용자;
protected User 이미지가_없는_사용자;

protected UserInformationDto 사용자_회원_정보 = new UserInformationDto(12345L);
protected UserInformationDto 탈퇴한_사용자_회원_정보 = new UserInformationDto(54321L);
protected UserInformationDto 가입하지_않은_사용자_회원_정보 = new UserInformationDto(-99999L);

protected String 유효한_액세스_토큰;
protected String 유효하지_않은_액세스_토큰 = "Bearer invalidAccessToken";
protected String 탈퇴한_사용자_액세스_토큰;
protected String 이미지가_없는_사용자_액세스_토큰;
protected String 존재하지_않는_사용자_액세스_토큰;
protected String 유효한_리프레시_토큰;
protected String 만료된_리프레시_토큰;
protected String 유효하지_않은_타입의_리프레시_토큰 = "invalidRefreshToken";

@MockBean
private DeviceTokenService deviceTokenService;

@Autowired
private JpaUserRepository userRepository;

@Autowired
private JpaProfileImageRepository profileImageRepository;

@Autowired
private TokenEncoder tokenEncoder;

@Autowired
private TokenDecoder tokenDecoder;

@Autowired
private BlackListTokenService blackListTokenService;

@Autowired
private JpaDeviceTokenRepository deviceTokenRepository;

@BeforeEach
void setUp() {
authenticationService = new AuthenticationService(
deviceTokenService,
소셜_회원_정보_제공자_묶음,
userRepository,
profileImageRepository,
tokenEncoder,
tokenDecoder,
blackListTokenService,
deviceTokenRepository
);

profileImageAuthenticationService = new AuthenticationService(
deviceTokenService,
소셜_회원_정보_제공자_묶음,
userRepository,
프로필_이미지_저장소,
tokenEncoder,
tokenDecoder,
blackListTokenService,
deviceTokenRepository
);

profileImageRepository.save(new ProfileImage("default_profile_image.png", "default_profile_image.png"));

사용자 = User.builder()
.name("kakao12345")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(0.0d)
.oauthId("12345")
.build();

탈퇴한_사용자 = User.builder()
.name("kakao12346")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(0.0d)
.oauthId("12346")
.build();

이미지가_없는_사용자 = User.builder()
.name("kakao12347")
.profileImage(null)
.reliability(0.0d)
.oauthId("12347")
.build();

userRepository.save(사용자);
userRepository.save(탈퇴한_사용자);

탈퇴한_사용자.withdrawal();

final DeviceToken deviceToken = new DeviceToken(사용자, 디바이스_토큰);
deviceTokenRepository.save(deviceToken);

유효한_리프레시_토큰 = tokenEncoder.encode(
LocalDateTime.now(),
TokenType.REFRESH,
Map.of("userId", 1L)
);

만료된_리프레시_토큰 = tokenEncoder.encode(
LocalDateTime.ofInstant(Instant.parse("2023-01-01T22:21:20Z"), ZoneId.of("UTC")),
TokenType.REFRESH,
Map.of("userId", 1L)
);

만료된_소셜_로그인_토큰 = tokenEncoder.encode(
Instant.parse("2000-08-10T15:30:00Z").atZone(ZoneId.of("UTC")).toLocalDateTime(),
TokenType.ACCESS,
Map.of("userId", 1L)
);

유효한_액세스_토큰 = tokenEncoder.encode(
LocalDateTime.now(),
TokenType.ACCESS,
Map.of("userId", 1L)
);

탈퇴한_사용자_액세스_토큰 = tokenEncoder.encode(
LocalDateTime.now(),
TokenType.ACCESS,
Map.of("userId", 탈퇴한_사용자.getId())
);

이미지가_없는_사용자_액세스_토큰 = tokenEncoder.encode(
LocalDateTime.now(),
TokenType.ACCESS,
Map.of("userId", 가입하지_않은_사용자_회원_정보.id())
);

존재하지_않는_사용자_액세스_토큰 = tokenEncoder.encode(
LocalDateTime.now(),
TokenType.ACCESS,
Map.of("userId", -99999L)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.ddang.ddang.authentication.application.fixture;

import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;

@SuppressWarnings("NonAsciiCharacters")
public class AuthenticationUserServiceFixture {

@Autowired
private JpaUserRepository userRepository;

protected User 사용자;
protected User 탈퇴한_사용자;

@BeforeEach
void setUp() {
사용자 = User.builder()
.name("kakao12345")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(0.0d)
.oauthId("12345")
.build();

탈퇴한_사용자 = User.builder()
.name("kakao12346")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(0.0d)
.oauthId("12346")
.build();

userRepository.save(사용자);
userRepository.save(탈퇴한_사용자);

탈퇴한_사용자.withdrawal();
}
}
Loading
Loading