diff --git a/src/test/java/org/creditto/authserver/certificate/service/CertificateServiceTest.java b/src/test/java/org/creditto/authserver/certificate/service/CertificateServiceTest.java new file mode 100644 index 0000000..b5ac5e0 --- /dev/null +++ b/src/test/java/org/creditto/authserver/certificate/service/CertificateServiceTest.java @@ -0,0 +1,216 @@ +package org.creditto.authserver.certificate.service; + +import org.creditto.authserver.auth.utils.CertificateEncryptionUtil; +import org.creditto.authserver.certificate.dto.CertificateIssueRequest; +import org.creditto.authserver.certificate.dto.CertificateSerialRequest; +import org.creditto.authserver.certificate.entity.Certificate; +import org.creditto.authserver.certificate.entity.CertificateUsageHistory; +import org.creditto.authserver.certificate.enums.CertificateStatus; +import org.creditto.authserver.certificate.repository.CertificateRepository; +import org.creditto.authserver.certificate.repository.CertificateUsageHistoryRepository; +import org.creditto.authserver.global.exception.InvalidSimplePasswordException; +import org.creditto.authserver.user.entity.User; +import org.creditto.authserver.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +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.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.creditto.authserver.auth.constants.ParameterConstants.CERTIFICATE_SERIAL; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CertificateServiceTest { + + @Mock + private UserRepository userRepository; + @Mock + private CertificateRepository certificateRepository; + @Mock + private CertificateUsageHistoryRepository certificateUsageHistoryRepository; + @Mock + private CertificateEncryptionUtil encryptionUtil; + + @InjectMocks + private CertificateService certificateService; + + private CertificateIssueRequest certificateIssueRequest; + private CertificateSerialRequest certificateSerialRequest; + private User registerUser; + private User authUser; + private User invalidAuthUser; + private User serialLookupUser; + + @BeforeEach + void setUp() { + certificateIssueRequest = new CertificateIssueRequest( + 1L, + "홍길동", + "010-1111-2222", + LocalDate.of(1990, 1, 1), + "938475" + ); + + certificateSerialRequest = new CertificateSerialRequest("홍길동", "010-7777-8888"); + + registerUser = User.create(new org.creditto.authserver.user.dto.UserRegisterRequest( + "홍길동", + LocalDate.of(1990, 1, 1), + "KR", + "010-1111-2222", + "서울시 종로구" + )); + ReflectionTestUtils.setField(registerUser, "id", 1L); + + authUser = User.create(new org.creditto.authserver.user.dto.UserRegisterRequest( + "홍길동", + LocalDate.of(1990, 1, 1), + "KR", + "010-2222-3333", + "서울시 종로구" + )); + ReflectionTestUtils.setField(authUser, "id", 2L); + + invalidAuthUser = User.create(new org.creditto.authserver.user.dto.UserRegisterRequest( + "홍길동", + LocalDate.of(1990, 1, 1), + "KR", + "010-4444-5555", + "서울시 종로구" + )); + ReflectionTestUtils.setField(invalidAuthUser, "id", 3L); + + serialLookupUser = User.create(new org.creditto.authserver.user.dto.UserRegisterRequest( + "홍길동", + LocalDate.of(1990, 1, 1), + "KR", + "010-7777-8888", + "서울시 종로구" + )); + ReflectionTestUtils.setField(serialLookupUser, "id", 4L); + } + + @Test + @DisplayName("인증서 발급 요청 시 RSA 키 생성과 사용 이력이 저장된다") + void issueCertificate_createsCertificate() throws Exception { + // given + when(userRepository.findById(certificateIssueRequest.userId())).thenReturn(Optional.of(registerUser)); + when(certificateRepository.existsCertificateByStatusAndUser(CertificateStatus.ACTIVE, registerUser)).thenReturn(false); + + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + when(encryptionUtil.generateRSAKeyPair()).thenReturn(keyPair); + when(encryptionUtil.encryptPrivateKey(eq(keyPair.getPrivate()), eq(certificateIssueRequest.simplePassword()), anyString())) + .thenReturn("encrypted-private-key"); + when(encryptionUtil.encodePublicKey(keyPair.getPublic())).thenReturn("encoded-public-key"); + when(certificateRepository.save(any(Certificate.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // when + var response = certificateService.issueCertificate(certificateIssueRequest, "127.0.0.1", "JUnit"); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Certificate.class); + verify(certificateRepository).save(captor.capture()); + Certificate saved = captor.getValue(); + assertThat(saved.getUser()).isEqualTo(registerUser); + assertThat(saved.getPublicKey()).isEqualTo("encoded-public-key"); + assertThat(response.serialNumber()).isEqualTo(saved.getSerialNumber()); + verify(certificateUsageHistoryRepository).save(any(CertificateUsageHistory.class)); + } + + @Test + @DisplayName("올바른 간편비밀번호로 인증 시 인증서가 반환된다") + void authenticateWithCertificate_success() throws Exception { + // given + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + Certificate certificate = Certificate.create( + authUser, + "serial-123", + Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()), + "encrypted-key", + "salt", + LocalDateTime.of(2024, 1, 1, 12, 0), + LocalDateTime.of(2099, 1, 1, 12, 0).plusYears(1) + ); + + when(certificateRepository.findBySerialNumber("serial-123")).thenReturn(Optional.of(certificate)); + when(encryptionUtil.decryptPrivateKey("encrypted-key", "123456", "salt")).thenReturn(keyPair.getPrivate()); + when(encryptionUtil.decodePublicKey(anyString())).thenReturn(keyPair.getPublic()); + when(encryptionUtil.verifyKeyPair(any(PrivateKey.class), any(PublicKey.class))).thenReturn(true); + + // when + Certificate authenticated = certificateService.authenticateWithCertificate("serial-123", "123456", "127.0.0.1", "JUnit"); + + // then + assertThat(authenticated.getSerialNumber()).isEqualTo("serial-123"); + verify(certificateUsageHistoryRepository).save(any(CertificateUsageHistory.class)); + } + + @Test + @DisplayName("간편비밀번호가 일치하지 않으면 인증 실패 예외를 던진다") + void authenticateWithCertificate_invalidPassword() throws Exception { + // given + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + Certificate certificate = Certificate.create( + invalidAuthUser, + "serial-999", + Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()), + "encrypted-key", + "salt", + LocalDateTime.now(), + LocalDateTime.now().plusYears(1) + ); + + when(certificateRepository.findBySerialNumber("serial-999")).thenReturn(Optional.of(certificate)); + when(encryptionUtil.decryptPrivateKey(anyString(), anyString(), anyString())).thenReturn(keyPair.getPrivate()); + when(encryptionUtil.decodePublicKey(anyString())).thenReturn(keyPair.getPublic()); + when(encryptionUtil.verifyKeyPair(any(PrivateKey.class), any(PublicKey.class))).thenReturn(false); + + // when & then + assertThatThrownBy(() -> certificateService.authenticateWithCertificate("serial-999", "123456", "127.0.0.1", "JUnit")) + .isInstanceOf(InvalidSimplePasswordException.class); + verify(certificateUsageHistoryRepository).save(any(CertificateUsageHistory.class)); + } + + @Test + @DisplayName("사용자 정보를 기반으로 활성 인증서 일련번호를 조회한다") + void getSerialNumberByUser_returnsActiveSerial() { + // given + Certificate certificate = Certificate.create( + serialLookupUser, + "serial-0001", + "public-key", + "private-key", + "salt", + LocalDateTime.now(), + LocalDateTime.now().plusYears(1) + ); + + when(userRepository.findByNameAndPhoneNo(certificateSerialRequest.username(), certificateSerialRequest.phoneNo())).thenReturn(Optional.of(serialLookupUser)); + when(certificateRepository.findByUserAndStatus(serialLookupUser, CertificateStatus.ACTIVE)).thenReturn(Optional.of(certificate)); + + // when + Map response = certificateService.getSerialNumberByUser(certificateSerialRequest); + + // then + assertThat(response).containsEntry(CERTIFICATE_SERIAL, "serial-0001"); + } + +} diff --git a/src/test/java/org/creditto/authserver/client/repository/JpaRegisteredClientRepositoryServiceTest.java b/src/test/java/org/creditto/authserver/client/repository/JpaRegisteredClientRepositoryServiceTest.java new file mode 100644 index 0000000..e6e3269 --- /dev/null +++ b/src/test/java/org/creditto/authserver/client/repository/JpaRegisteredClientRepositoryServiceTest.java @@ -0,0 +1,97 @@ +package org.creditto.authserver.client.repository; + +import org.creditto.authserver.client.entity.OAuth2RegisteredClient; +import org.creditto.authserver.client.entity.RegisteredClientMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; + +import java.time.Instant; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JpaRegisteredClientRepositoryServiceTest { + + @Mock + private OAuth2RegisteredClientRepository registeredClientRepository; + @Mock + private RegisteredClientMapper mapper; + + @InjectMocks + private JpaRegisteredClientRepositoryService service; + + private RegisteredClient registeredClient; + + @BeforeEach + void setUp() { + Instant fixedInstant = Instant.parse("2024-01-01T10:00:00Z"); + registeredClient = RegisteredClient.withId("registered-client-id") + .clientId("client-id") + .clientSecret("secret") + .clientIdIssuedAt(fixedInstant) + .clientSecretExpiresAt(fixedInstant.plusSeconds(3600)) + .clientName("테스트 클라이언트") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read") + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) + .tokenSettings(TokenSettings.builder().reuseRefreshTokens(true).build()) + .build(); + } + + @Test + @DisplayName("RegisteredClient 저장 시 엔티티로 변환하여 저장소에 위임한다") + void save_delegatesToJpaRepository() { + // given + OAuth2RegisteredClient entity = mock(OAuth2RegisteredClient.class); + when(mapper.convertToEntity(registeredClient)).thenReturn(entity); + + // when + service.save(registeredClient); + + // then + verify(registeredClientRepository).save(entity); + } + + @Test + @DisplayName("ID 기준 조회는 Optional 값을 DTO로 변환해 반환한다") + void findById_returnsRegisteredClient() { + // given + OAuth2RegisteredClient entity = mock(OAuth2RegisteredClient.class); + + when(registeredClientRepository.findById("registered-client-id")).thenReturn(Optional.of(entity)); + when(mapper.convertToRegisteredClient(entity)).thenReturn(registeredClient); + + // when + RegisteredClient result = service.findById("registered-client-id"); + + // then + assertThat(result).isEqualTo(registeredClient); + } + + @Test + @DisplayName("ClientId 기준 조회도 존재하지 않으면 null을 반환한다") + void findByClientId_returnsNullWhenMissing() { + // given + when(registeredClientRepository.findByClientId("missing")).thenReturn(Optional.empty()); + + // when + RegisteredClient result = service.findByClientId("missing"); + + // then + assertThat(result).isNull(); + } + +} diff --git a/src/test/java/org/creditto/authserver/client/service/JpaOAuth2AuthorizationServiceTest.java b/src/test/java/org/creditto/authserver/client/service/JpaOAuth2AuthorizationServiceTest.java new file mode 100644 index 0000000..a3ddabb --- /dev/null +++ b/src/test/java/org/creditto/authserver/client/service/JpaOAuth2AuthorizationServiceTest.java @@ -0,0 +1,124 @@ +package org.creditto.authserver.client.service; + +import org.creditto.authserver.client.entity.OAuth2AuthorizationEntity; +import org.creditto.authserver.client.repository.OAuth2AuthorizationRepository; +import org.junit.jupiter.api.BeforeEach; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; + +import java.time.Instant; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class JpaOAuth2AuthorizationServiceTest { + + @Mock + private OAuth2AuthorizationRepository authorizationRepository; + @Mock + private RegisteredClientRepository registeredClientRepository; + + private JpaOAuth2AuthorizationService service; + private RegisteredClient registeredClient; + private OAuth2Authorization authorization; + + @BeforeEach + void setUp() { + service = new JpaOAuth2AuthorizationService(authorizationRepository, registeredClientRepository); + registeredClient = RegisteredClient.withId("registered-client-id") + .clientId("client-id") + .clientSecret("secret") + .clientName("테스트 클라이언트") + .clientAuthenticationMethod(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read") + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) + .tokenSettings(TokenSettings.builder().reuseRefreshTokens(true).build()) + .build(); + + Instant issuedAt = Instant.parse("2024-01-01T10:00:00Z"); + OAuth2AccessToken token = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "access-token", + issuedAt, + issuedAt.plusSeconds(120), + Set.of("read") + ); + + authorization = OAuth2Authorization.withRegisteredClient(registeredClient) + .id("authorization-id") + .principalName("principal-user") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .authorizedScopes(Set.of("read")) + .attribute(OAuth2ParameterNames.STATE, "state-value") + .token(token, metadata -> metadata.put("meta", "value")) + .build(); + } + + @Test + @DisplayName("Authorization 저장 시 JPA 엔티티로 변환되어 저장된다") + void save_persistsConvertedEntity() { + // given + when(authorizationRepository.findById(authorization.getId())).thenReturn(Optional.empty()); + + // when + service.save(authorization); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(OAuth2AuthorizationEntity.class); + verify(authorizationRepository).save(captor.capture()); + OAuth2AuthorizationEntity entity = captor.getValue(); + assertThat(entity.getPrincipalName()).isEqualTo("principal-user"); + assertThat(entity.getAccessTokenValue()).isEqualTo("access-token"); + assertThat(entity.getAuthorizedScopes()).contains("read"); + } + + @Test + @DisplayName("액세스 토큰으로 Authorization을 조회하면 도메인 객체로 복원된다") + void findByToken_returnsAuthorization() { + // given + AtomicReference storedEntity = new AtomicReference<>(); + when(authorizationRepository.findById("authorization-id")).thenReturn(Optional.empty()); + when(authorizationRepository.save(any(OAuth2AuthorizationEntity.class))).thenAnswer(invocation -> { + OAuth2AuthorizationEntity entity = invocation.getArgument(0); + storedEntity.set(entity); + return entity; + }); + + service.save(authorization); + + when(authorizationRepository.findByAccessTokenValue("access-token")) + .thenReturn(Optional.ofNullable(storedEntity.get())); + when(registeredClientRepository.findById("registered-client-id")).thenReturn(registeredClient); + + // when + OAuth2Authorization result = service.findByToken("access-token", OAuth2TokenType.ACCESS_TOKEN); + + // then + assertThat(result).isNotNull(); + assertThat(result.getPrincipalName()).isEqualTo("principal-user"); + OAuth2Authorization.Token token = result.getToken(OAuth2AccessToken.class); + assertThat(token).isNotNull(); + assertThat(token.getToken().getTokenValue()).isEqualTo("access-token"); + } + +} diff --git a/src/test/java/org/creditto/authserver/user/service/UserServiceTest.java b/src/test/java/org/creditto/authserver/user/service/UserServiceTest.java new file mode 100644 index 0000000..04a5cfb --- /dev/null +++ b/src/test/java/org/creditto/authserver/user/service/UserServiceTest.java @@ -0,0 +1,126 @@ +package org.creditto.authserver.user.service; + +import org.creditto.authserver.user.dto.UserRegisterRequest; +import org.creditto.authserver.user.dto.UserResponse; +import org.creditto.authserver.user.entity.User; +import org.creditto.authserver.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.test.util.ReflectionTestUtils; + +import jakarta.persistence.EntityNotFoundException; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserService userService; + + private UserRegisterRequest baseRequest; + private UserRegisterRequest duplicateRequest; + private UserRegisterRequest secondRequest; + + @BeforeEach + void setUp() { + baseRequest = new UserRegisterRequest( + "홍길동", + LocalDate.of(1990, 1, 1), + "KR", + "010-1111-2222", + "서울시 종로구" + ); + + duplicateRequest = new UserRegisterRequest( + "홍길동", + LocalDate.of(1990, 1, 1), + "KR", + "010-3333-4444", + "서울시 종로구" + ); + + secondRequest = new UserRegisterRequest( + "홍길동", + LocalDate.of(1990, 1, 1), + "KR", + "010-2222-3333", + "서울시 종로구" + ); + } + + @Test + @DisplayName("사용자 등록 시 신규 사용자 정보를 저장한다") + void registerUser_savesNewUser() { + // given + User savedUser = User.create(baseRequest); + ReflectionTestUtils.setField(savedUser, "id", 1L); + + when(userRepository.findByPhoneNo(baseRequest.phoneNo())).thenReturn(Optional.empty()); + when(userRepository.save(any(User.class))).thenReturn(savedUser); + + // when + UserResponse response = userService.registerUser(baseRequest); + + // then + assertThat(response.userId()).isEqualTo(1L); + assertThat(response.name()).isEqualTo(baseRequest.name()); + verify(userRepository, times(1)).save(any(User.class)); + } + + @Test + @DisplayName("중복 전화번호로 사용자 등록 시 예외가 발생한다") + void registerUser_duplicatePhoneThrowsException() { + // given + when(userRepository.findByPhoneNo(duplicateRequest.phoneNo())).thenReturn(Optional.of(User.create(duplicateRequest))); + + // when & then + assertThatThrownBy(() -> userService.registerUser(duplicateRequest)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("ID로 사용자 조회 시 존재하지 않으면 예외를 던진다") + void getUser_notFoundThrowsException() { + // given + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userService.getUser(1L)) + .isInstanceOf(EntityNotFoundException.class); + } + + @Test + @DisplayName("전체 사용자 조회 시 응답 DTO 목록을 반환한다") + void getAllUsers_returnsResponses() { + // given + User first = User.create(baseRequest); + User second = User.create(secondRequest); + ReflectionTestUtils.setField(first, "id", 10L); + ReflectionTestUtils.setField(second, "id", 11L); + when(userRepository.findAll()).thenReturn(List.of(first, second)); + + // when + List responses = userService.getAllUsers(); + + // then + assertThat(responses).hasSize(2); + assertThat(responses.get(0).userId()).isEqualTo(10L); + assertThat(responses.get(1).phoneNo()).isEqualTo("010-2222-3333"); + } +}