|
| 1 | +package com.yapp.datastore |
| 2 | + |
| 3 | +import androidx.datastore.core.DataStore |
| 4 | +import androidx.datastore.preferences.core.PreferenceDataStoreFactory |
| 5 | +import androidx.datastore.preferences.core.Preferences |
| 6 | +import androidx.datastore.preferences.core.booleanPreferencesKey |
| 7 | +import androidx.datastore.preferences.core.edit |
| 8 | +import kotlinx.coroutines.flow.first |
| 9 | +import kotlinx.coroutines.test.runTest |
| 10 | +import org.junit.Assert.assertEquals |
| 11 | +import org.junit.Rule |
| 12 | +import org.junit.Test |
| 13 | +import org.junit.rules.TemporaryFolder |
| 14 | +import java.time.Clock |
| 15 | +import java.time.Instant |
| 16 | +import java.time.LocalDate |
| 17 | +import java.time.ZoneOffset |
| 18 | +import java.util.UUID |
| 19 | + |
| 20 | +class FortunePreferencesTest { |
| 21 | + |
| 22 | + @get:Rule |
| 23 | + val temporaryFolder = TemporaryFolder() |
| 24 | + |
| 25 | + private val fixedZoneOffsetUtc = ZoneOffset.UTC |
| 26 | + |
| 27 | + private val baseInstantAtT0: Instant = Instant.parse("2025-09-17T00:00:00Z") |
| 28 | + private val baseInstantAtT0Plus2Seconds: Instant = Instant.parse("2025-09-17T00:00:02Z") |
| 29 | + |
| 30 | + private val fixedClockAtT0: Clock = Clock.fixed(baseInstantAtT0, fixedZoneOffsetUtc) |
| 31 | + private val fixedClockAtT0Plus2Seconds: Clock = |
| 32 | + Clock.fixed(baseInstantAtT0Plus2Seconds, fixedZoneOffsetUtc) |
| 33 | + |
| 34 | + private val referenceInstantForAnyDay: Instant = Instant.parse("2025-09-17T00:00:00Z") |
| 35 | + private val fixedClockForReferenceDay: Clock = |
| 36 | + Clock.fixed(referenceInstantForAnyDay, fixedZoneOffsetUtc) |
| 37 | + |
| 38 | + private fun createNewDataStoreWithFile(fileName: String): DataStore<Preferences> = |
| 39 | + PreferenceDataStoreFactory.create { |
| 40 | + temporaryFolder.newFile(fileName) |
| 41 | + } |
| 42 | + |
| 43 | + private fun createFortunePreferencesWithClock( |
| 44 | + dataStore: DataStore<Preferences>, |
| 45 | + fixedClock: Clock, |
| 46 | + ): FortunePreferences = FortunePreferences(dataStore, fixedClock) |
| 47 | + |
| 48 | + @Test |
| 49 | + fun `운세_생성_상태_Creating_만료_시_Failure로_교정된다`() = runTest { |
| 50 | + // given: t0 시점에서 Creating(lease 1초) 설정 |
| 51 | + val dataStoreAtT0 = createNewDataStoreWithFile("prefs_expire.preferences_pb") |
| 52 | + val preferencesAtT0 = |
| 53 | + createFortunePreferencesWithClock(dataStoreAtT0, fixedClockAtT0) |
| 54 | + |
| 55 | + val generatedAttemptId = UUID.randomUUID().toString() |
| 56 | + preferencesAtT0.markFortuneCreating(attemptId = generatedAttemptId, lease = 1_000L) |
| 57 | + |
| 58 | + // when: t0 + 2초 경과 후 같은 DataStore를 새로운 Clock으로 읽음 |
| 59 | + val preferencesAtT0Plus2Seconds = |
| 60 | + createFortunePreferencesWithClock(dataStoreAtT0, fixedClockAtT0Plus2Seconds) |
| 61 | + |
| 62 | + // then: Creating → false, Failed → true |
| 63 | + val creating = preferencesAtT0Plus2Seconds.isFortuneCreatingFlow.first() |
| 64 | + assertEquals(false, creating) |
| 65 | + val failed = preferencesAtT0Plus2Seconds.isFortuneFailedFlow.first() |
| 66 | + assertEquals(true, failed) |
| 67 | + } |
| 68 | + |
| 69 | + @Test |
| 70 | + fun `만료_정보_없는_운세_생성_상태_Creating은_즉시_Failure로_교정된다`() = runTest { |
| 71 | + // given: 과거 버전 데이터 (Creating=true만 존재) |
| 72 | + val dataStoreWithLegacyCreating = createNewDataStoreWithFile("prefs_legacy.preferences_pb") |
| 73 | + val keyCreatingOnly = booleanPreferencesKey("fortune_creating") |
| 74 | + dataStoreWithLegacyCreating.edit { it[keyCreatingOnly] = true } |
| 75 | + |
| 76 | + val preferencesFromLegacyData = |
| 77 | + createFortunePreferencesWithClock(dataStoreWithLegacyCreating, fixedClockForReferenceDay) |
| 78 | + |
| 79 | + // when: Failure로 교정 로직이 표시된 Flow 구독 시작 |
| 80 | + |
| 81 | + // then: 즉시 Creating → false, Failed → true |
| 82 | + val creating = preferencesFromLegacyData.isFortuneCreatingFlow.first() |
| 83 | + assertEquals(false, creating) |
| 84 | + val failed = preferencesFromLegacyData.isFortuneFailedFlow.first() |
| 85 | + assertEquals(true, failed) |
| 86 | + } |
| 87 | + |
| 88 | + @Test |
| 89 | + fun `생성_성공_시_attemptId가_일치할_때만_운세_생성_상태가_Creating에서_Success로_전환된다`() = runTest { |
| 90 | + // given: 운세 Creating 상태 |
| 91 | + val dataStore = createNewDataStoreWithFile("prefs_success.preferences_pb") |
| 92 | + val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) |
| 93 | + val validAttemptId = "ATTEMPT_VALID" |
| 94 | + val invalidAttemptId = "ATTEMPT_INVALID" |
| 95 | + val createdFortuneId = 99L |
| 96 | + preferences.markFortuneCreating(attemptId = validAttemptId, lease = 60_000L) |
| 97 | + |
| 98 | + // when: 잘못된 attemptId로 생성 성공 처리 시도 |
| 99 | + preferences.markFortuneCreatedIfAttemptMatches( |
| 100 | + attemptId = invalidAttemptId, |
| 101 | + fortuneId = createdFortuneId |
| 102 | + ) |
| 103 | + |
| 104 | + // then: 여전히 Creating 상태 (무시됨) |
| 105 | + val stillCreating = preferences.isFortuneCreatingFlow.first() |
| 106 | + assertEquals(true, stillCreating) |
| 107 | + |
| 108 | + // when: 올바른 attemptId로 생성 성공 처리 시도 |
| 109 | + preferences.markFortuneCreatedIfAttemptMatches( |
| 110 | + attemptId = validAttemptId, |
| 111 | + fortuneId = createdFortuneId |
| 112 | + ) |
| 113 | + |
| 114 | + // then: Creating → false, Failed → false, fortuneId 및 날짜 설정 |
| 115 | + val creatingAfterSuccess = preferences.isFortuneCreatingFlow.first() |
| 116 | + assertEquals(false, creatingAfterSuccess) |
| 117 | + val failedAfterSuccess = preferences.isFortuneFailedFlow.first() |
| 118 | + assertEquals(false, failedAfterSuccess) |
| 119 | + val savedId = preferences.fortuneIdFlow.first() |
| 120 | + assertEquals(createdFortuneId, savedId) |
| 121 | + val savedEpoch = preferences.fortuneDateEpochFlow.first() |
| 122 | + assertEquals(LocalDate.now(fixedClockForReferenceDay).toEpochDay(), savedEpoch) |
| 123 | + } |
| 124 | + |
| 125 | + @Test |
| 126 | + fun `운세_생성_실패_시_attemptId가_일치할_때만_Failure로_전환된다`() = runTest { |
| 127 | + // given: 운세 Creating 상태 |
| 128 | + val dataStore = createNewDataStoreWithFile("prefs_fail.preferences_pb") |
| 129 | + val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) |
| 130 | + val validAttemptId = "ATTEMPT_VALID" |
| 131 | + val invalidAttemptId = "ATTEMPT_INVALID" |
| 132 | + preferences.markFortuneCreating(attemptId = validAttemptId, lease = 60_000L) |
| 133 | + |
| 134 | + // when: 잘못된 attemptId로 실패 처리 시도 |
| 135 | + preferences.markFortuneFailedIfAttemptMatches(invalidAttemptId) |
| 136 | + |
| 137 | + // then: 아직 Creating 상태 (무시됨) |
| 138 | + val stillCreating = preferences.isFortuneCreatingFlow.first() |
| 139 | + assertEquals(true, stillCreating) |
| 140 | + |
| 141 | + // when: 올바른 attemptId로 실패 처리 시도 |
| 142 | + preferences.markFortuneFailedIfAttemptMatches(validAttemptId) |
| 143 | + |
| 144 | + // then: Creating → false, Failed → true |
| 145 | + val creatingAfterFail = preferences.isFortuneCreatingFlow.first() |
| 146 | + assertEquals(false, creatingAfterFail) |
| 147 | + val failed = preferences.isFortuneFailedFlow.first() |
| 148 | + assertEquals(true, failed) |
| 149 | + } |
| 150 | + |
| 151 | + @Test |
| 152 | + fun `오늘_운세가_있고_확인한_경우_hasUnseenFortune가_false`() = runTest { |
| 153 | + // given: 오늘 운세가 생성되어 있고(미확인) |
| 154 | + val dataStore = createNewDataStoreWithFile("prefs_seen.preferences_pb") |
| 155 | + val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) |
| 156 | + val attemptId = "ATTEMPT_FOR_SEEN" |
| 157 | + val fortuneId = 777L |
| 158 | + preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L) |
| 159 | + preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId) |
| 160 | + |
| 161 | + // when: 사용자가 오늘 운세를 확인 |
| 162 | + preferences.markFortuneSeen() |
| 163 | + |
| 164 | + // then: hasUnseenFortune → false |
| 165 | + val unseen = preferences.hasUnseenFortuneFlow.first() |
| 166 | + assertEquals(false, unseen) |
| 167 | + } |
| 168 | + |
| 169 | + @Test |
| 170 | + fun `오늘_운세가_있고_아직_확인하지_않은_경우_hasUnseenFortune가_true`() = runTest { |
| 171 | + // given: 오늘 운세가 생성되어 있는 상태(미확인) |
| 172 | + val dataStore = createNewDataStoreWithFile("prefs_unseen.preferences_pb") |
| 173 | + val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) |
| 174 | + val attemptId = "ATTEMPT_FOR_UNSEEN" |
| 175 | + val fortuneId = 123L |
| 176 | + preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L) |
| 177 | + preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId) |
| 178 | + |
| 179 | + // when: hasUnseenFortuneFlow 구독 |
| 180 | + |
| 181 | + // then: 오늘 운세 존재 + 아직 읽지 않음 = hasUnseenFortune → true |
| 182 | + val unseen = preferences.hasUnseenFortuneFlow.first() |
| 183 | + assertEquals(true, unseen) |
| 184 | + } |
| 185 | + |
| 186 | + @Test |
| 187 | + fun `오늘_운세가_있고_Tooltip을_보여주었다면_shouldShowFortuneToolTip이_false`() = runTest { |
| 188 | + // given: 오늘 운세가 생성되어 있는 상태(툴팁 미표시) |
| 189 | + val dataStore = createNewDataStoreWithFile("prefs_tooltip_true.preferences_pb") |
| 190 | + val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) |
| 191 | + val attemptId = "ATTEMPT_FOR_TOOLTIP_TRUE" |
| 192 | + val fortuneId = 888L |
| 193 | + preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L) |
| 194 | + preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId) |
| 195 | + |
| 196 | + // when: ToolTip을 보여줌 |
| 197 | + preferences.markFortuneTooltipShown() |
| 198 | + |
| 199 | + // then: shouldShowFortuneToolTip → false |
| 200 | + val showTooltip = preferences.shouldShowFortuneToolTipFlow.first() |
| 201 | + assertEquals(false, showTooltip) |
| 202 | + } |
| 203 | + |
| 204 | + @Test |
| 205 | + fun `오늘_운세가_있고_Tooltip을_아직_보여주지_않았다면_shouldShowFortuneToolTip이_true`() = runTest { |
| 206 | + // given: 오늘 운세가 생성되어 있는 상태(툴팁 미표시) |
| 207 | + val dataStore = createNewDataStoreWithFile("prefs_tooltip.preferences_pb") |
| 208 | + val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) |
| 209 | + val attemptId = "ATTEMPT_FOR_TOOLTIP" |
| 210 | + val fortuneId = 456L |
| 211 | + preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L) |
| 212 | + preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId) |
| 213 | + |
| 214 | + // when: shouldShowFortuneToolTipFlow 구독 |
| 215 | + |
| 216 | + // then: 오늘 운세 존재 + 툴팁 미표시 = shouldShowFortuneToolTip → true |
| 217 | + val showTooltip = preferences.shouldShowFortuneToolTipFlow.first() |
| 218 | + assertEquals(true, showTooltip) |
| 219 | + } |
| 220 | +} |
0 commit comments