diff --git a/TEST_STRATEGY.md b/TEST_STRATEGY.md index 6b88a8c79..d25595cc2 100644 --- a/TEST_STRATEGY.md +++ b/TEST_STRATEGY.md @@ -73,6 +73,11 @@ verification 모듈의 수동 QA 시나리오는 `VERIFICATION_TEST_PLAN.md` 를 - 미검증: 실기기의 갤러리 URI 접근·실서버 사진 반영·이미지 로딩 결과와 글꼴 크기별 점수 정렬. 실행 조건이 없는 종료·동기화·GPS·Jank·간헐적 오류는 이번 수정 범위에서 제외했다. +### 2026-10-29 무음 동기화 푸시 (#639) + +- `RuleUpMessagingServiceTest`: sync 요청 푸시가 알림 없이 catch-up 작업을 예약하는지, 실제 스케줄러를 연결하면 네트워크 조건이 지정된 WorkManager 작업이 등록되는지 검증한다. +- 실제 FCM 전달과 WorkManager 실행은 실기기 및 서버에서 확인해야 한다. + ### 2026-09-30 TTI Firebase Performance 연동 (#518) - `ObservabilityTtiShooterTest` 5건: 저장된 전체·구간 시간(0ms 포함), 미완성 기록 제외, diff --git a/app/src/main/java/com/ruleup/android_ruleup/push/RuleUpMessagingService.kt b/app/src/main/java/com/ruleup/android_ruleup/push/RuleUpMessagingService.kt index 44d7460fb..7deaae445 100644 --- a/app/src/main/java/com/ruleup/android_ruleup/push/RuleUpMessagingService.kt +++ b/app/src/main/java/com/ruleup/android_ruleup/push/RuleUpMessagingService.kt @@ -5,6 +5,7 @@ import com.google.firebase.messaging.RemoteMessage import com.ruleup.observability.domain.api.Observability import com.ruleup.observability.domain.api.d import com.ruleup.observability.domain.api.w +import com.ruleup.verification.domain.repository.SyncScheduler import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -18,6 +19,7 @@ private const val TAG = "[Push]" // 페이로드 명세의 type 딱지. private const val TYPE_SETUP_REQUIRED = "SETUP_REQUIRED" private const val TYPE_PERMISSION_REQUIRED = "PERMISSION_REQUIRED" +private const val TYPE_SYNC_REQUIRED = "SYNC_REQUIRED" /** FCM 수신 진입점. */ @AndroidEntryPoint @@ -31,6 +33,9 @@ class RuleUpMessagingService : FirebaseMessagingService() { @Inject lateinit var notificationPresenter: NotificationPresenter + @Inject + lateinit var syncScheduler: SyncScheduler + private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override fun onNewToken(token: String) { @@ -45,6 +50,7 @@ class RuleUpMessagingService : FirebaseMessagingService() { when (val type = data["type"]) { // 무음 쪽지: 알림 없음. TYPE_SETUP_REQUIRED, TYPE_PERMISSION_REQUIRED -> Unit + TYPE_SYNC_REQUIRED -> syncScheduler.enqueueCatchUp() else -> { val push = diff --git a/app/src/test/java/com/ruleup/android_ruleup/push/RuleUpMessagingServiceTest.kt b/app/src/test/java/com/ruleup/android_ruleup/push/RuleUpMessagingServiceTest.kt new file mode 100644 index 000000000..68c983098 --- /dev/null +++ b/app/src/test/java/com/ruleup/android_ruleup/push/RuleUpMessagingServiceTest.kt @@ -0,0 +1,67 @@ +package com.ruleup.android_ruleup.push + +import android.app.Application +import androidx.work.Configuration +import androidx.work.NetworkType +import androidx.work.WorkManager +import com.google.firebase.messaging.RemoteMessage +import com.ruleup.observability.domain.test.testObservability +import com.ruleup.verification.data.sync.VerificationSyncSchedulerImpl +import com.ruleup.verification.domain.repository.SyncScheduler +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class, sdk = [33]) +class RuleUpMessagingServiceTest { + @Test + fun `sync 요청 푸시를 받으면 네트워크가 연결된 경우 실행할 작업이 등록된다`() { + val context = RuntimeEnvironment.getApplication() + WorkManager.initialize(context, Configuration.Builder().build()) + val manager = WorkManager.getInstance(context) + val service = + RuleUpMessagingService().apply { + syncScheduler = VerificationSyncSchedulerImpl(context) + observability = testObservability() + } + + service.onMessageReceived(RemoteMessage.Builder("test").addData("type", "SYNC_REQUIRED").build()) + + val work = manager.getWorkInfosForUniqueWork(VerificationSyncSchedulerImpl.CATCH_UP_WORK_NAME).get(10, TimeUnit.SECONDS).single() + assertEquals(NetworkType.CONNECTED, work.constraints.requiredNetworkType) + } + + @Test + fun `sync 요청 푸시는 알림 없이 catch-up 동기화를 예약한다`() { + val scheduler = FakeSyncScheduler() + val service = + RuleUpMessagingService().apply { + syncScheduler = scheduler + observability = testObservability() + } + val message = RemoteMessage.Builder("test").addData("type", "SYNC_REQUIRED").build() + + service.onMessageReceived(message) + + assertEquals(1, scheduler.catchUpCount) + } + + private class FakeSyncScheduler : SyncScheduler { + var catchUpCount = 0 + + override fun enqueueCatchUp() { + catchUpCount++ + } + + override fun cancel() = Unit + + override fun ensureScheduled() = Unit + + override fun reschedule(flushIntervalSec: Int) = Unit + } +}