diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c1c10460..d4b0db9c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,9 +2,9 @@ name: Java CI for Picke on: push: - branches: [ "develop", "main" ] + branches: [ "dev", "main" ] pull_request: - branches: [ "develop", "main" ] + branches: [ "dev", "main" ] jobs: build: @@ -21,6 +21,9 @@ jobs: distribution: 'temurin' cache: gradle # 1. 빌드 속도를 획기적으로 줄이기 위해 그래들 캐시 적용 + - name: Install ffmpeg + run: sudo apt-get update && sudo apt-get install -y ffmpeg + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/src/main/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcher.java b/src/main/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcher.java index ca52e36d..2c5148e3 100644 --- a/src/main/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcher.java +++ b/src/main/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcher.java @@ -5,9 +5,9 @@ import com.swyp.picke.domain.notification.repository.NotificationScheduleRepository; import com.swyp.picke.domain.notification.service.NotificationDispatchService; import com.swyp.picke.domain.notification.service.NotificationService; +import java.time.Clock; import java.time.LocalDate; import java.time.LocalTime; -import java.time.ZoneId; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,17 +24,16 @@ @RequiredArgsConstructor public class NotificationScheduleDispatcher { - private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul"); - private final NotificationScheduleRepository notificationScheduleRepository; private final NotificationService notificationService; private final NotificationDispatchService notificationDispatchService; + private final Clock clock; @Scheduled(cron = "0 * * * * *", zone = "Asia/Seoul") @Transactional public void dispatchDueSchedules() { - LocalTime now = LocalTime.now(SEOUL_ZONE); - LocalDate today = LocalDate.now(SEOUL_ZONE); + LocalTime now = LocalTime.now(clock); + LocalDate today = LocalDate.now(clock); List dueSchedules = notificationScheduleRepository.findAllByEnabledTrue().stream() .filter(schedule -> schedule.isDue(now, today)) diff --git a/src/main/java/com/swyp/picke/global/config/SchedulerConfig.java b/src/main/java/com/swyp/picke/global/config/SchedulerConfig.java index 0a51ead4..927d7746 100644 --- a/src/main/java/com/swyp/picke/global/config/SchedulerConfig.java +++ b/src/main/java/com/swyp/picke/global/config/SchedulerConfig.java @@ -1,5 +1,7 @@ package com.swyp.picke.global.config; +import java.time.Clock; +import java.time.ZoneId; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; @@ -20,4 +22,9 @@ public ThreadPoolTaskScheduler taskScheduler() { scheduler.setThreadNamePrefix("credit-scheduler-"); return scheduler; } + + @Bean + public Clock clock() { + return Clock.system(ZoneId.of("Asia/Seoul")); + } } diff --git a/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java b/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java index 6ef1d455..a51cf36c 100644 --- a/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java +++ b/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java @@ -99,7 +99,7 @@ void createBattle_persistsAllMappedFields() throws Exception { LocalDate targetDate = LocalDate.now(); Tag category = createTag("battle-category", TagType.CATEGORY); - Tag philosopher = createTag("battle-philosopher", TagType.PHILOSOPHER); + Tag philosopher = createTag("소크라테스", TagType.PHILOSOPHER); Tag value = createTag("battle-value", TagType.VALUE); Map payload = Map.of( @@ -395,6 +395,9 @@ private User createAdminUser() { } private Tag createTag(String prefix, TagType type) { + if (type == TagType.PHILOSOPHER) { + return tagRepository.save(Tag.builder().name(prefix).type(type).build()); + } String normalizedPrefix = prefix.length() > 10 ? prefix.substring(0, 10) : prefix; return tagRepository.save( Tag.builder() diff --git a/src/test/java/com/swyp/picke/domain/admin/controller/AdminNoticeIntegrationTest.java b/src/test/java/com/swyp/picke/domain/admin/controller/AdminNoticeIntegrationTest.java index d06b4681..17e314a3 100644 --- a/src/test/java/com/swyp/picke/domain/admin/controller/AdminNoticeIntegrationTest.java +++ b/src/test/java/com/swyp/picke/domain/admin/controller/AdminNoticeIntegrationTest.java @@ -26,9 +26,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -98,21 +96,14 @@ void admin_can_create_and_list_notices() throws Exception { } @Test - @DisplayName("admin notice page form flow persists notice and user can fetch it") - void admin_notice_page_form_flow_persists_notice_and_user_can_fetch_it() throws Exception { + @DisplayName("admin notice creation flow persists notice and user can fetch it") + void admin_notice_creation_flow_persists_notice_and_user_can_fetch_it() throws Exception { String adminToken = createAdminToken(); String userToken = createUserToken(); String marker = UUID.randomUUID().toString().substring(0, 8); String title = "ui-notice-" + marker; String body = "ui-body-" + marker; - mockMvc.perform(get("/api/v1/admin/picke/notice")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("id=\"notice-form\""))) - .andExpect(content().string(containsString("id=\"notice-title\""))) - .andExpect(content().string(containsString("id=\"notice-body\""))) - .andExpect(content().string(containsString("/js/admin/notice/notice.js"))); - Map payload = Map.of( "category", "NOTICE", "title", title, diff --git a/src/test/java/com/swyp/picke/domain/battle/service/BattleProposalServiceTest.java b/src/test/java/com/swyp/picke/domain/battle/service/BattleProposalServiceTest.java index 53b664e7..da0ef1f0 100644 --- a/src/test/java/com/swyp/picke/domain/battle/service/BattleProposalServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/battle/service/BattleProposalServiceTest.java @@ -60,8 +60,8 @@ void propose_Success() { // then // 제안 저장 메서드가 호출되었는지 확인 verify(battleProposalRepository, times(1)).save(any()); - // 크레딧 차감(-30) 로직이 호출되었는지 확인 - verify(creditService, times(1)).addCredit(eq(1L), eq(CreditType.TOPIC_SUGGEST), eq(-30), any()); + // 크레딧 차감(-100) 로직이 호출되었는지 확인 + verify(creditService, times(1)).addCredit(eq(1L), eq(CreditType.TOPIC_SUGGEST), eq(-100), any()); } @Test diff --git a/src/test/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcherTest.java b/src/test/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcherTest.java index 34628809..a4460e89 100644 --- a/src/test/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcherTest.java +++ b/src/test/java/com/swyp/picke/domain/notification/scheduler/NotificationScheduleDispatcherTest.java @@ -12,18 +12,22 @@ import com.swyp.picke.domain.notification.repository.NotificationScheduleRepository; import com.swyp.picke.domain.notification.service.NotificationDispatchService; import com.swyp.picke.domain.notification.service.NotificationService; +import java.time.Clock; import java.time.LocalTime; +import java.time.ZoneId; import java.util.List; +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; @ExtendWith(MockitoExtension.class) class NotificationScheduleDispatcherTest { + private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul"); + @Mock private NotificationScheduleRepository notificationScheduleRepository; @@ -33,13 +37,21 @@ class NotificationScheduleDispatcherTest { @Mock private NotificationDispatchService notificationDispatchService; - @InjectMocks private NotificationScheduleDispatcher notificationScheduleDispatcher; + @BeforeEach + void setUp() { + Clock fixedClock = Clock.fixed( + LocalTime.of(9, 0).atDate(java.time.LocalDate.now(SEOUL_ZONE)).atZone(SEOUL_ZONE).toInstant(), + SEOUL_ZONE); + notificationScheduleDispatcher = new NotificationScheduleDispatcher( + notificationScheduleRepository, notificationService, notificationDispatchService, fixedClock); + } + @Test @DisplayName("현재 시각과 일치하는 활성 예약만 발송하고 발송일을 기록한다") void dispatchDueSchedules_sendsOnlyMatchingEnabledSchedules() { - LocalTime now = LocalTime.now(); + LocalTime now = LocalTime.of(9, 0); NotificationSchedule due = NotificationSchedule.builder() .title("오늘의 질문") .subtitle("지금 확인해보세요") diff --git a/src/test/java/com/swyp/picke/domain/user/service/CreditServiceTest.java b/src/test/java/com/swyp/picke/domain/user/service/CreditServiceTest.java index bd207d58..db84c0db 100644 --- a/src/test/java/com/swyp/picke/domain/user/service/CreditServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/user/service/CreditServiceTest.java @@ -41,6 +41,12 @@ class CreditServiceTest { @Mock private UserService userService; + @Mock + private com.swyp.picke.domain.notification.service.NotificationService notificationService; + + @Mock + private com.swyp.picke.domain.battle.repository.BattleRepository battleRepository; + @InjectMocks private CreditService creditService; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 8a951d1f..437dfdd2 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -25,6 +25,17 @@ spring: credentials: location: file:/tmp/dummy.json +firebase: + credentials: + location: src/test/resources/firebase-test-credentials.json + +admob: + app-id: dummy + reward: + unit-id: + ios: dummy + android: dummy + oauth: kakao: client-id: dummy @@ -32,6 +43,11 @@ oauth: google: client-id: dummy client-secret: dummy + apple: + team-id: dummy + client-id: dummy + key-id: dummy + private-key: dummy openai: api-key: dummy diff --git a/src/test/resources/firebase-test-credentials.json b/src/test/resources/firebase-test-credentials.json new file mode 100644 index 00000000..7400b1ba --- /dev/null +++ b/src/test/resources/firebase-test-credentials.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "picke-test-project", + "private_key_id": "dummykeyid1234567890", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDC8cIyqMc1Aey1\n9QVQ4QmSiWpiOPy4pFN09XmprAuZZDWbFaKD724NhbXgdtcEHNewYAWWpN3FsyBh\nh9YZPHXHa0dfdSsEKaKDxrIv/7YX8M/kRbDMSH35yDZZTIoxZnJLNZm+Zs2sB7L0\nYtzbT1ctnHV2r+zlVC29S2OV7/LY5YRbMM7fhvwu+ww+Ri6GY3Nx5dhvi/yzgeT8\njLw4hGx9uSMBZnNmpxp+hGL/wo2LSD4FQ0PrAqJTkLu90tky51XFmUGVmGqnWpRE\nuLw4C83x1xIyNayU5pR9oDTdLh+CGkVb8QOuJS95tLjqo3hx4CiQICXA+Ul8282R\nqIyrvFFbAgMBAAECggEAFLGvSMtr4i+jHim1d8F7z6dwuJ6ODVe8WEUati1CSfU+\nT4k7aEAJcbwI85wJ9TDOoLWAwl4cALmkLVZLHwCxDAtSV0rL1zRIQS7diYTeoqn4\nl6XiP71OSi67vj0GynmyllNJT9H/8Uwb7h90jH9epMPgIEpKnomSFW8kUi1XnTiI\nPCJJBzodYbSLFLpApum22E76Zjj4CDbKUnRZVYF/0GxesXhGCVlpgVi7XsuWLKvd\n7ScH9d2VxEkrvlj71Mwt2kV7791oCWIiPD/iy1i6WE3/H1YJj6LnGWPZUppvvXsH\nPfJbZNVyXMFrGJP9Ql+bAT1C0zo1z6Mudp7V8Ai3iQKBgQD/InZT5sGnLJasSEH1\njfhiGX4yyQ7Uw5Eikx6I2QQGzwxNBAu5wII8O+TryNQPu2lsBzIcvyL5LNHyNfPn\nQwTk7DWiWxZi18GBJen7IoVVDGL+bubrKSAGroNYVpsYJvJDZxV2wpXHig3wUXmP\n4Gz/FDKFHh7GAFvD0Jk+HQfpvQKBgQDDmwg6OeWnlujRGm8IGOWcu7pOEKLTxlQ9\ndB0kxRGWlslsWZtcFj/YmjExr0w3OZaHLrb+vBmFGDZqK19AUt6Nm7RGPUq6P+XG\nT1bK+Vsi3NSLYRbdLaT3s42n/8Hb+ZPx4HyQTGq/nfgciJCdvtzncDeDsgNIpxyk\n+SDNSu+89wKBgBZmdjEjn3kIByqVJYVjs50ZU+UtlenESefZNuMY+quGXjQc2NK0\nPjr/nze8aDIBaF4du56egXmTH9O+PO3fCnz26Daa/Los60Zlh8eO3ln7Pm3MWuXm\ntHMhu1J0OCXEtZyJXm8Q4omka1jgLmYddDRpF45seJM10Ni+ZdX4QouZAoGAGgCZ\n72OS69xbxrBE4kas/1DVS1taydwrhp/Q3/pyhBo3XHfs9yjeA+U7dOdgslatc/r5\nyJMosVCuqx5o4xwhCaIRLOUo8elcmigh2YmcW94PQxf8+hn/PA5aXmLZWmyrBhRZ\nerUt25scSG6/Crk8lGeOeatIVHgijquveJrlk7ECgYEAxQeYN0uzLBneootOeqLs\nGQ7ubWqJpgm0IeZST9rUA2OBjk4AwHBEC3tyayH4XEbTG259dIUYCe6xp3pjbpyH\nl/d7pFsau+D9yC72yeN+g2bZUKt7yUfmLIUDODXNF+WH9a+xFFDgL7r/zHNI0NDc\niXuwmsuc6msGwp7959s8nD0=\n-----END PRIVATE KEY-----\n", + "client_email": "test-fcm@picke-test-project.iam.gserviceaccount.com", + "client_id": "000000000000000000000", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-fcm%40picke-test-project.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} \ No newline at end of file