diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3109497d..54d5168f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,46 +1,48 @@ name: ci on: - pull_request: - branches: [ "main", "develop" ] + pull_request: + branches: [ "main", "develop" ] jobs: - build: - name: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + build: + name: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: true + token: ${{ secrets.MOABAM_SUBMODULE_KEY }} - - name: JDK 17 셋업 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'corretto' + - name: JDK 17 셋업 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'corretto' - - name: Gradle 캐싱 - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- + - name: Gradle 캐싱 + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- - - name: Gradle Grant 권한 부여 - run: chmod +x gradlew + - name: Gradle Grant 권한 부여 + run: chmod +x gradlew - - name: SonarCloud 캐싱 - uses: actions/cache@v3 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar + - name: SonarCloud 캐싱 + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar - - name: 빌드 및 분석 - run: ./gradlew build jacocoTestReport sonar --info --stacktrace - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_CLOUD_TOKEN }} + - name: 빌드 및 분석 + run: ./gradlew build jacocoTestReport sonar --info --stacktrace + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_CLOUD_TOKEN }} diff --git a/build.gradle b/build.gradle index 0943c572..26a9be58 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,12 @@ dependencies { // Configuration Binding annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // Firebase Admin + implementation 'com.google.firebase:firebase-admin:9.2.0' } tasks.named('test') { diff --git a/src/main/java/com/moabam/api/application/NotificationService.java b/src/main/java/com/moabam/api/application/NotificationService.java new file mode 100644 index 00000000..797ae048 --- /dev/null +++ b/src/main/java/com/moabam/api/application/NotificationService.java @@ -0,0 +1,50 @@ +package com.moabam.api.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import com.moabam.api.domain.repository.NotificationRepository; +import com.moabam.api.dto.NotificationMapper; +import com.moabam.global.common.annotation.MemberTest; +import com.moabam.global.error.exception.ConflictException; +import com.moabam.global.error.exception.NotFoundException; +import com.moabam.global.error.model.ErrorMessage; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NotificationService { + + private final FirebaseMessaging firebaseMessaging; + private final NotificationRepository notificationRepository; + + @Transactional + public void sendKnockNotification(MemberTest member, Long targetId, Long roomId) { + validateFcmToken(targetId); + validateConflictKnockNotification(member.memberId(), targetId, roomId); + + String fcmToken = notificationRepository.findFcmTokenByMemberId(targetId); + Notification notification = NotificationMapper.toKnockNotificationEntity(member.nickname()); + Message message = NotificationMapper.toMessageEntity(notification, fcmToken); + + firebaseMessaging.sendAsync(message); + notificationRepository.saveKnockNotification(member.memberId(), targetId, roomId); + } + + private void validateFcmToken(Long memberId) { + if (!notificationRepository.existsFcmTokenByMemberId(memberId)) { + throw new NotFoundException(ErrorMessage.FCM_TOKEN_NOT_FOUND); + } + } + + private void validateConflictKnockNotification(Long memberId, Long targetId, Long roomId) { + if (notificationRepository.existsKnockByMemberId(memberId, targetId, roomId)) { + throw new ConflictException(ErrorMessage.KNOCK_CONFLICT); + } + } +} diff --git a/src/main/java/com/moabam/api/domain/repository/NotificationRepository.java b/src/main/java/com/moabam/api/domain/repository/NotificationRepository.java new file mode 100644 index 00000000..ce705d68 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/NotificationRepository.java @@ -0,0 +1,52 @@ +package com.moabam.api.domain.repository; + +import static com.moabam.global.common.util.GlobalConstant.*; +import static java.util.Objects.*; + +import java.time.Duration; + +import org.springframework.stereotype.Repository; + +import com.moabam.global.common.repository.StringRedisRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class NotificationRepository { + + private final StringRedisRepository stringRedisRepository; + + // TODO : 세연님 로그인 시, 해당 메서드 사용해서 해당 유저의 FCM TOKEN 저장하면 됩니다. + public void saveFcmToken(Long key, String value) { + stringRedisRepository.save( + String.valueOf(requireNonNull(key)), + requireNonNull(value), + requireNonNull(Duration.ofDays(EXPIRE_FCM_TOKEN)) + ); + } + + public void saveKnockNotification(Long memberId, Long targetId, Long roomId) { + String key = requireNonNull(roomId) + UNDER_BAR + requireNonNull(memberId) + TO + requireNonNull(targetId); + stringRedisRepository.save(key, BLANK, requireNonNull(Duration.ofHours(EXPIRE_KNOCK))); + } + + // TODO : 세연님 로그아웃 시, 해당 메서드 사용해서 해당 유저의 FCM TOKEN 삭제하시면 됩니다. + public void deleteFcmTokenByMemberId(Long memberId) { + stringRedisRepository.delete(String.valueOf(requireNonNull(memberId))); + } + + public String findFcmTokenByMemberId(Long memberId) { + return stringRedisRepository.get(String.valueOf(requireNonNull(memberId))); + } + + public boolean existsFcmTokenByMemberId(Long memberId) { + return stringRedisRepository.hasKey(String.valueOf(requireNonNull(memberId))); + } + + public boolean existsKnockByMemberId(Long memberId, Long targetId, Long roomId) { + String key = requireNonNull(roomId) + UNDER_BAR + requireNonNull(memberId) + TO + requireNonNull(targetId); + + return stringRedisRepository.hasKey(key); + } +} diff --git a/src/main/java/com/moabam/api/dto/NotificationMapper.java b/src/main/java/com/moabam/api/dto/NotificationMapper.java new file mode 100644 index 00000000..f51016be --- /dev/null +++ b/src/main/java/com/moabam/api/dto/NotificationMapper.java @@ -0,0 +1,28 @@ +package com.moabam.api.dto; + +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NotificationMapper { + + private static final String TITLE = "모아밤"; + private static final String KNOCK_BODY = "님이 콕 찔렀습니다."; + + public static Notification toKnockNotificationEntity(String nickname) { + return Notification.builder() + .setTitle(TITLE) + .setBody(nickname + KNOCK_BODY) + .build(); + } + + public static Message toMessageEntity(Notification notification, String fcmToken) { + return Message.builder() + .setNotification(notification) + .setToken(fcmToken) + .build(); + } +} diff --git a/src/main/java/com/moabam/global/common/annotation/MemberTest.java b/src/main/java/com/moabam/global/common/annotation/MemberTest.java new file mode 100644 index 00000000..a1449cba --- /dev/null +++ b/src/main/java/com/moabam/global/common/annotation/MemberTest.java @@ -0,0 +1,8 @@ +package com.moabam.global.common.annotation; + +public record MemberTest( + Long memberId, + String nickname +) { + +} diff --git a/src/main/java/com/moabam/global/common/repository/StringRedisRepository.java b/src/main/java/com/moabam/global/common/repository/StringRedisRepository.java new file mode 100644 index 00000000..3677942c --- /dev/null +++ b/src/main/java/com/moabam/global/common/repository/StringRedisRepository.java @@ -0,0 +1,39 @@ +package com.moabam.global.common.repository; + +import java.time.Duration; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StringRedisRepository { + + private final StringRedisTemplate stringRedisTemplate; + + @Transactional + public void save(String key, String value, Duration timeout) { + stringRedisTemplate + .opsForValue() + .set(key, value, timeout); + } + + @Transactional + public void delete(String key) { + stringRedisTemplate.delete(key); + } + + public String get(String key) { + return stringRedisTemplate + .opsForValue() + .get(key); + } + + public Boolean hasKey(String email) { + return stringRedisTemplate.hasKey(email); + } +} diff --git a/src/main/java/com/moabam/global/common/util/GlobalConstant.java b/src/main/java/com/moabam/global/common/util/GlobalConstant.java index 5b87cd58..a74b791d 100644 --- a/src/main/java/com/moabam/global/common/util/GlobalConstant.java +++ b/src/main/java/com/moabam/global/common/util/GlobalConstant.java @@ -6,6 +6,13 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GlobalConstant { + public static final String BLANK = ""; public static final String COMMA = ","; + public static final String UNDER_BAR = "_"; public static final String CHARSET_UTF_8 = ";charset=UTF-8"; + + public static final String TO = "_TO_"; + public static final long EXPIRE_KNOCK = 12; + public static final long EXPIRE_FCM_TOKEN = 60; + public static final String FIREBASE_PATH = "config/moabam-firebase.json"; } diff --git a/src/main/java/com/moabam/global/config/FcmConfig.java b/src/main/java/com/moabam/global/config/FcmConfig.java new file mode 100644 index 00000000..7c95478d --- /dev/null +++ b/src/main/java/com/moabam/global/config/FcmConfig.java @@ -0,0 +1,46 @@ +package com.moabam.global.config; + +import static com.moabam.global.common.util.GlobalConstant.*; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.scheduling.annotation.EnableScheduling; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import com.moabam.global.error.exception.FcmException; +import com.moabam.global.error.model.ErrorMessage; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +@EnableScheduling +public class FcmConfig { + + @Bean + public FirebaseMessaging firebaseMessaging() { + try (InputStream inputStream = new ClassPathResource(FIREBASE_PATH).getInputStream()) { + GoogleCredentials credentials = GoogleCredentials.fromStream(inputStream); + FirebaseOptions firebaseOptions = FirebaseOptions.builder() + .setCredentials(credentials) + .build(); + + if (FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(firebaseOptions); + log.info("======= Firebase init start ======="); + } + + return FirebaseMessaging.getInstance(); + } catch (IOException e) { + log.error("======= firebase moabam error =======\n" + e); + throw new FcmException(ErrorMessage.FCM_INIT_FAILED); + } + } +} diff --git a/src/main/java/com/moabam/global/config/RedisConfig.java b/src/main/java/com/moabam/global/config/RedisConfig.java new file mode 100644 index 00000000..d4d484c2 --- /dev/null +++ b/src/main/java/com/moabam/global/config/RedisConfig.java @@ -0,0 +1,34 @@ +package com.moabam.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); + stringRedisTemplate.setKeySerializer(new StringRedisSerializer()); + stringRedisTemplate.setValueSerializer(new StringRedisSerializer()); + stringRedisTemplate.setConnectionFactory(redisConnectionFactory); + + return stringRedisTemplate; + } +} diff --git a/src/main/java/com/moabam/global/error/exception/FcmException.java b/src/main/java/com/moabam/global/error/exception/FcmException.java new file mode 100644 index 00000000..25c9fa48 --- /dev/null +++ b/src/main/java/com/moabam/global/error/exception/FcmException.java @@ -0,0 +1,10 @@ +package com.moabam.global.error.exception; + +import com.moabam.global.error.model.ErrorMessage; + +public class FcmException extends MoabamException { + + public FcmException(ErrorMessage errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/com/moabam/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/moabam/global/error/handler/GlobalExceptionHandler.java index 2653c8d5..2dc1f65d 100644 --- a/src/main/java/com/moabam/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/moabam/global/error/handler/GlobalExceptionHandler.java @@ -13,6 +13,7 @@ import com.moabam.global.error.exception.BadRequestException; import com.moabam.global.error.exception.ConflictException; +import com.moabam.global.error.exception.FcmException; import com.moabam.global.error.exception.ForbiddenException; import com.moabam.global.error.exception.MoabamException; import com.moabam.global.error.exception.NotFoundException; @@ -53,10 +54,22 @@ protected ErrorResponse handleBadRequestException(MoabamException moabamExceptio return new ErrorResponse(moabamException.getMessage(), null); } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(FcmException.class) + protected ErrorResponse handleFcmException(MoabamException moabamException) { + return new ErrorResponse(moabamException.getMessage(), null); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(NullPointerException.class) + protected ErrorResponse handleNullPointerException(NullPointerException exception) { + return new ErrorResponse(exception.getMessage(), null); + } + @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) - public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException moabamException) { - List fieldErrors = moabamException.getBindingResult().getFieldErrors(); + protected ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { + List fieldErrors = exception.getBindingResult().getFieldErrors(); Map validation = new HashMap<>(); for (FieldError fieldError : fieldErrors) { diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index f874a1d3..b4302eeb 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -21,7 +21,11 @@ public enum ErrorMessage { INVALID_BUG_COUNT("벌레 개수는 0 이상이어야 합니다."), INVALID_PRICE("가격은 0 이상이어야 합니다."), - INVALID_QUANTITY("수량은 1 이상이어야 합니다."); + INVALID_QUANTITY("수량은 1 이상이어야 합니다."), + + FCM_INIT_FAILED("파이어베이스 설정을 실패했습니다."), + FCM_TOKEN_NOT_FOUND("해당 유저는 접속 중이 아닙니다."), + KNOCK_CONFLICT("이미 콕 알림을 보낸 대상입니다."); private final String message; } diff --git a/src/main/resources/config b/src/main/resources/config index 90404393..ab594df9 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 90404393aafb50e5650b81f6b67b69adc825e938 +Subproject commit ab594df9fcbf13159da3bb2fbb57d792b3f9f0f9 diff --git a/src/test/java/com/moabam/MoabamServerApplicationTests.java b/src/test/java/com/moabam/MoabamServerApplicationTests.java index ec2bac91..3eabd1b5 100644 --- a/src/test/java/com/moabam/MoabamServerApplicationTests.java +++ b/src/test/java/com/moabam/MoabamServerApplicationTests.java @@ -1,13 +1,8 @@ package com.moabam; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class MoabamServerApplicationTests { - @Test - void contextLoads() { - } - } diff --git a/src/test/java/com/moabam/api/application/NotificationServiceTest.java b/src/test/java/com/moabam/api/application/NotificationServiceTest.java new file mode 100644 index 00000000..e21a1750 --- /dev/null +++ b/src/test/java/com/moabam/api/application/NotificationServiceTest.java @@ -0,0 +1,83 @@ +package com.moabam.api.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +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 com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.moabam.api.domain.repository.NotificationRepository; +import com.moabam.global.common.annotation.MemberTest; +import com.moabam.global.error.exception.ConflictException; +import com.moabam.global.error.exception.NotFoundException; +import com.moabam.global.error.model.ErrorMessage; + +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + + @InjectMocks + private NotificationService notificationService; + + @Mock + private NotificationRepository notificationRepository; + + @Mock + private FirebaseMessaging firebaseMessaging; + + private MemberTest memberTest; + + @BeforeEach + void setUp() { + memberTest = new MemberTest(2L, "nickname"); + } + + @DisplayName("성공적으로 상대를 콕 찔렀을 때, - Void") + @Test + void notificationService_sendKnockNotification() { + // Given + given(notificationRepository.existsFcmTokenByMemberId(any(Long.class))).willReturn(true); + given(notificationRepository.existsKnockByMemberId(any(Long.class), any(Long.class), any(Long.class))) + .willReturn(false); + given(notificationRepository.findFcmTokenByMemberId(any(Long.class))).willReturn("FCM-TOKEN"); + + // When + notificationService.sendKnockNotification(memberTest, 2L, 1L); + + // Then + verify(firebaseMessaging).sendAsync(any(Message.class)); + verify(notificationRepository).saveKnockNotification(any(Long.class), any(Long.class), any(Long.class)); + } + + @DisplayName("콕 찌를 상대의 FCM 토큰이 존재하지 않을 때, - NotFoundException") + @Test + void notificationService_sendKnockNotification_NotFoundException() { + // Given + given(notificationRepository.existsFcmTokenByMemberId(any(Long.class))).willReturn(false); + + // When & Then + assertThatThrownBy(() -> notificationService.sendKnockNotification(memberTest, 1L, 1L)) + .isInstanceOf(NotFoundException.class) + .hasMessage(ErrorMessage.FCM_TOKEN_NOT_FOUND.getMessage()); + } + + @DisplayName("콕 찌를 상대가 이미 찌른 상대일 때, - ConflictException") + @Test + void notificationService_sendKnockNotification_ConflictException() { + // Given + given(notificationRepository.existsFcmTokenByMemberId(any(Long.class))).willReturn(true); + given(notificationRepository.existsKnockByMemberId(any(Long.class), any(Long.class), any(Long.class))) + .willReturn(true); + + // When & Then + assertThatThrownBy(() -> notificationService.sendKnockNotification(memberTest, 1L, 1L)) + .isInstanceOf(ConflictException.class) + .hasMessage(ErrorMessage.KNOCK_CONFLICT.getMessage()); + } +} diff --git a/src/test/java/com/moabam/api/domain/repository/NotificationRepositoryTest.java b/src/test/java/com/moabam/api/domain/repository/NotificationRepositoryTest.java new file mode 100644 index 00000000..87c1c5c7 --- /dev/null +++ b/src/test/java/com/moabam/api/domain/repository/NotificationRepositoryTest.java @@ -0,0 +1,133 @@ +package com.moabam.api.domain.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.Duration; + +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 com.moabam.global.common.repository.StringRedisRepository; + +@ExtendWith(MockitoExtension.class) +class NotificationRepositoryTest { + + @InjectMocks + private NotificationRepository notificationRepository; + + @Mock + private StringRedisRepository stringRedisRepository; + + @DisplayName("FCM 토큰이 성공적으로 저장 될 때, - Void") + @Test + void notificationRepository_saveFcmToken() { + // When + notificationRepository.saveFcmToken(1L, "value1"); + + // Then + verify(stringRedisRepository).save(any(String.class), any(String.class), any(Duration.class)); + } + + @DisplayName("FCM 토큰 저장 시, 필요한 값이 NULL 일 때, - NullPointerException") + @Test + void notificationRepository_save_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.saveFcmToken(null, "value")) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("콕 알림이 성공적으로 저장 될 때, - Void") + @Test + void notificationRepository_saveKnockNotification() { + // When + notificationRepository.saveKnockNotification(1L, 2L, 1L); + + // Then + verify(stringRedisRepository).save(any(String.class), any(String.class), any(Duration.class)); + } + + @DisplayName("콕 알림 저장 시, 필요한 값이 NULL 일 때, - NullPointerException") + @Test + void notificationRepository_saveKnockNotification_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.saveKnockNotification(null, 2L, 1L)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("FCM 토큰이 성공적으로 삭제 될 때, - Void") + @Test + void notificationRepository_deleteFcmTokenByMemberId() { + // When + notificationRepository.deleteFcmTokenByMemberId(1L); + + // Then + verify(stringRedisRepository).delete(any(String.class)); + } + + @DisplayName("FCM 토큰 삭제 시, 필요한 값이 NULL 일 때, - NullPointerException") + @Test + void notificationRepository_deleteFcmTokenByMemberId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.deleteFcmTokenByMemberId(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("FCM 토큰을 성공적으로 조회할 때, - (String) FCM TOKEN") + @Test + void notificationRepository_findFcmTokenByMemberId() { + // When + notificationRepository.findFcmTokenByMemberId(1L); + + // Then + verify(stringRedisRepository).get(any(String.class)); + } + + @DisplayName("FCM 토큰 조회 시, 필요한 값이 NULL 일 때, - NullPointerException") + @Test + void notificationRepository_findFcmTokenByMemberId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.findFcmTokenByMemberId(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("FCM 토큰 존재 여부를 성공적으로 확인 할 때, - Boolean") + @Test + void notificationRepository_existsFcmTokenByMemberId() { + // When + notificationRepository.existsFcmTokenByMemberId(1L); + + // Then + verify(stringRedisRepository).hasKey(any(String.class)); + } + + @DisplayName("FCM 토큰 존재 여부 체크 시, 필요한 값이 NULL 일 때, - NullPointerException") + @Test + void notificationRepository_existsFcmTokenByMemberId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.existsFcmTokenByMemberId(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("콕 알림 여부 체크를 정상적으로 확인할 때, - Boolean") + @Test + void notificationRepository_existsKnockByMemberId() { + // When + notificationRepository.existsKnockByMemberId(1L, 2L, 1L); + + // Then + verify(stringRedisRepository).hasKey(any(String.class)); + } + + @DisplayName("콕 알림 여부 체크 시, 필요한 값이 NULL 일 때, - NullPointerException") + @Test + void notificationRepository_existsKnockByMemberId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.existsKnockByMemberId(null, 2L, 1L)) + .isInstanceOf(NullPointerException.class); + } +} diff --git a/src/test/java/com/moabam/api/presentation/BugControllerTest.java b/src/test/java/com/moabam/api/presentation/BugControllerTest.java index 4bfedb04..28881031 100644 --- a/src/test/java/com/moabam/api/presentation/BugControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/BugControllerTest.java @@ -13,6 +13,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import com.fasterxml.jackson.databind.ObjectMapper; @@ -23,6 +24,7 @@ @SpringBootTest @AutoConfigureMockMvc +@ActiveProfiles("test") class BugControllerTest { @Autowired diff --git a/src/test/java/com/moabam/api/presentation/ProductControllerTest.java b/src/test/java/com/moabam/api/presentation/ProductControllerTest.java index f18b3236..338ed65b 100644 --- a/src/test/java/com/moabam/api/presentation/ProductControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/ProductControllerTest.java @@ -15,6 +15,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,6 +27,7 @@ @SpringBootTest @AutoConfigureMockMvc +@ActiveProfiles("test") class ProductControllerTest { @Autowired diff --git a/src/test/java/com/moabam/global/common/repository/StringRedisRepositoryTest.java b/src/test/java/com/moabam/global/common/repository/StringRedisRepositoryTest.java new file mode 100644 index 00000000..b2f424fa --- /dev/null +++ b/src/test/java/com/moabam/global/common/repository/StringRedisRepositoryTest.java @@ -0,0 +1,74 @@ +package com.moabam.global.common.repository; + +import static org.mockito.BDDMockito.*; + +import java.time.Duration; + +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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +@ExtendWith(MockitoExtension.class) +class StringRedisRepositoryTest { + + @InjectMocks + private StringRedisRepository stringRedisRepository; + + @Mock + private StringRedisTemplate stringRedisTemplate; + + @Spy + private ValueOperations valueOperations; + + @DisplayName("레디스에 문자열 데이터가 성공적으로 저장될 때, - Void") + @Test + void string_redis_repository_save() { + // Given + given(stringRedisTemplate.opsForValue()).willReturn(valueOperations); + + // When + stringRedisRepository.save("key1", "value", Duration.ofHours(1)); + + // Then + verify(stringRedisTemplate.opsForValue()).set(any(String.class), any(String.class), any(Duration.class)); + } + + @DisplayName("레디스의 특정 데이터가 성공적으로 삭제될 때, - Void") + @Test + void string_redis_repository_delete() { + // When + stringRedisRepository.delete("key2"); + + // Then + verify(stringRedisTemplate).delete(any(String.class)); + } + + @DisplayName("레디스의 특정 데이터가 성공적으로 조회될 때, - String(Value)") + @Test + void string_redis_repository_get() { + // Given + given(stringRedisTemplate.opsForValue()).willReturn(valueOperations); + + // When + stringRedisRepository.get("key3"); + + // Then + verify(stringRedisTemplate.opsForValue()).get(any(String.class)); + } + + @DisplayName("레디스의 특정 데이터 존재 여부를 성공적으로 체크할 때, - Boolean") + @Test + void string_redis_repository_hasKey() { + // When + stringRedisRepository.hasKey("not found key"); + + // Then + verify(stringRedisTemplate).hasKey(any(String.class)); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 94864159..33df62ba 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -12,3 +12,12 @@ oauth2: authorization_uri: https://authorization.com/test/test redirect_uri: http://redirect:8080/test token_uri: https://token.com/test/test + +# Spring +spring: + + # Redis + data: + redis: + host: 127.0.0.1 + port: 6379