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 9 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

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
@@ -0,0 +1,161 @@
package com.ddang.ddang.authentication.application.fixture;

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.configuration.IsolateDatabase;
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 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 UserInformationDto 사용자_회원_정보 = new UserInformationDto(12345L);
protected UserInformationDto 탈퇴한_사용자_회원_정보 = new UserInformationDto(54321L);
protected UserInformationDto 가입하지_않은_사용자_회원_정보 = new UserInformationDto(-99999L);

protected String 유효한_액세스_토큰;
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
);

사용자 = 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();

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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,48 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.ddang.ddang.authentication.configuration.JwtConfigurationProperties;
import com.ddang.ddang.authentication.domain.TokenType;
import com.ddang.ddang.authentication.domain.exception.InvalidTokenException;
import java.time.LocalDateTime;
import java.util.Map;
import com.ddang.ddang.authentication.infrastructure.jwt.fixture.JwtDecoderFixture;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;

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

JwtDecoder jwtDecoder;

JwtEncoder jwtEncoder;

@BeforeEach
void setUp() {
final JwtConfigurationProperties jwtConfigurationProperties = new JwtConfigurationProperties(
"thisistoolargeaccesstokenkeyfordummykeydataforlocal",
"thisistoolargerefreshtokenkeyfordummykeydataforlocal",
12L,
1460L
);

jwtDecoder = new JwtDecoder(jwtConfigurationProperties);
jwtEncoder = new JwtEncoder(jwtConfigurationProperties);
}
class JwtDecoderTest extends JwtDecoderFixture {

@Test
void 토큰의_길이가_유효하지_않다면_예외가_발생한다() {
// given
final String invalidLengthToken = "abcde";
final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정);

// when & then
assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, invalidLengthToken))
assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, 유효하지_않은_길이의_토큰))
.isInstanceOf(InvalidTokenException.class)
.hasMessage("Bearer 타입이 아니거나 유효한 토큰이 아닙니다.");
}

@Test
void 토큰의_타입이_유효하지_않다면_예외가_발생한다() {
// given
final String invalidTypeToken = "Basic12 abcde";
final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정);

// when & then
assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, invalidTypeToken))
assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, 유효하지_않은_타입의_토큰))
.isInstanceOf(InvalidTokenException.class)
.hasMessage("Bearer 타입이 아닙니다.");
}

@Test
void 토큰이_유효하지_않다면_빈_Optional을_반환한다() {
// given
final String invalidToken = "Bearer adf";
final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정);

// when
final Optional<PrivateClaims> actual = jwtDecoder.decode(TokenType.ACCESS, invalidToken);
final Optional<PrivateClaims> actual = jwtDecoder.decode(TokenType.ACCESS, 유효하지_않은_토큰);

// then
assertThat(actual).isEmpty();
Expand All @@ -72,13 +53,15 @@ void setUp() {
@Test
void 유효한_토큰이면_해당_토큰의_본문을_반환한다() {
// given
final Map<String, Object> privateClaims = Map.of("userId", 1L);
final String accessToken = jwtEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, privateClaims);
final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정);

// when
final Optional<PrivateClaims> actual = jwtDecoder.decode(TokenType.ACCESS, accessToken);
final Optional<PrivateClaims> actual = jwtDecoder.decode(TokenType.ACCESS, 유효한_토큰);

// then
assertThat(actual).isPresent();
SoftAssertions.assertSoftly(softAssertions -> {
softAssertions.assertThat(actual).isPresent();
softAssertions.assertThat(actual.get().userId()).isEqualTo(토큰_내용.get(회원_아이디_키));
});
}
}
Loading