Skip to content

Commit 0d7bac0

Browse files
committed
Feat: AuthTokenDataStore 구현
- AuthTokenDataStore 인터페이스 추가 - AuthTokenDataStoreImpl 구현체 추가
1 parent 8852dff commit 0d7bac0

3 files changed

Lines changed: 314 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.threegap.bitnagil.datastore.storage
2+
3+
import com.threegap.bitnagil.datastore.model.AuthToken
4+
import kotlinx.coroutines.flow.Flow
5+
6+
interface AuthTokenDataStore {
7+
val tokenFlow: Flow<AuthToken>
8+
9+
suspend fun updateAuthToken(authToken: AuthToken): AuthToken
10+
11+
suspend fun updateAccessToken(accessToken: String): AuthToken
12+
13+
suspend fun updateRefreshToken(refreshToken: String): AuthToken
14+
15+
suspend fun clearAuthToken(): AuthToken
16+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.threegap.bitnagil.datastore.storage
2+
3+
import android.util.Log
4+
import androidx.datastore.core.DataStore
5+
import com.threegap.bitnagil.datastore.model.AuthToken
6+
import kotlinx.coroutines.flow.Flow
7+
import javax.inject.Inject
8+
9+
internal class AuthTokenDataStoreImpl
10+
@Inject
11+
constructor(
12+
private val dataStore: DataStore<AuthToken>,
13+
) : AuthTokenDataStore {
14+
override val tokenFlow: Flow<AuthToken> = dataStore.data
15+
16+
override suspend fun updateAuthToken(authToken: AuthToken): AuthToken =
17+
runCatching {
18+
dataStore.updateData { authToken }
19+
}.fold(
20+
onSuccess = { it },
21+
onFailure = {
22+
Log.e(TAG, "updateAuthToken failed:", it)
23+
throw it
24+
},
25+
)
26+
27+
override suspend fun updateAccessToken(accessToken: String): AuthToken =
28+
runCatching {
29+
dataStore.updateData { authToke ->
30+
authToke.copy(accessToken = accessToken)
31+
}
32+
}.fold(
33+
onSuccess = { it },
34+
onFailure = {
35+
Log.e(TAG, "updateAccessToken failed:", it)
36+
throw it
37+
},
38+
)
39+
40+
override suspend fun updateRefreshToken(refreshToken: String): AuthToken =
41+
runCatching {
42+
dataStore.updateData { authToken ->
43+
authToken.copy(refreshToken = refreshToken)
44+
}
45+
}.fold(
46+
onSuccess = { it },
47+
onFailure = {
48+
Log.e(TAG, "updateRefreshToken failed:", it)
49+
throw it
50+
},
51+
)
52+
53+
override suspend fun clearAuthToken(): AuthToken =
54+
runCatching {
55+
dataStore.updateData { AuthToken() }
56+
}.fold(
57+
onSuccess = { it },
58+
onFailure = {
59+
Log.e(TAG, "clearAuthToken failed:", it)
60+
throw it
61+
},
62+
)
63+
64+
companion object {
65+
private const val TAG = "AuthTokenDataStore"
66+
}
67+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package com.threegap.bitnagil.datastore.storage
2+
3+
import androidx.datastore.core.DataStore
4+
import androidx.datastore.core.DataStoreFactory
5+
import androidx.datastore.core.Serializer
6+
import com.threegap.bitnagil.datastore.model.AuthToken
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.flow.first
9+
import kotlinx.coroutines.flow.flowOf
10+
import kotlinx.coroutines.test.runTest
11+
import kotlinx.coroutines.withContext
12+
import kotlinx.serialization.json.Json
13+
import org.junit.Assert.assertEquals
14+
import org.junit.Before
15+
import org.junit.Rule
16+
import org.junit.Test
17+
import org.junit.rules.TemporaryFolder
18+
import java.io.InputStream
19+
import java.io.OutputStream
20+
21+
class AuthTokenDataStoreImplTest {
22+
@get:Rule
23+
val temporaryFolder: TemporaryFolder =
24+
TemporaryFolder
25+
.builder()
26+
.assureDeletion()
27+
.build()
28+
29+
private lateinit var dataStore: DataStore<AuthToken>
30+
private lateinit var authTokenDataStore: AuthTokenDataStore
31+
32+
object FakeAuthTokenSerializer : Serializer<AuthToken> {
33+
override val defaultValue: AuthToken
34+
get() = AuthToken()
35+
36+
override suspend fun readFrom(input: InputStream): AuthToken {
37+
return try {
38+
input.bufferedReader().use {
39+
Json.decodeFromString(AuthToken.serializer(), it.readText())
40+
}
41+
} catch (e: Exception) {
42+
AuthToken()
43+
}
44+
}
45+
46+
override suspend fun writeTo(
47+
t: AuthToken,
48+
output: OutputStream,
49+
) {
50+
withContext(Dispatchers.IO) {
51+
output.writer().use {
52+
it.write(Json.encodeToString(AuthToken.serializer(), t))
53+
}
54+
}
55+
}
56+
}
57+
58+
@Before
59+
fun setup() {
60+
dataStore =
61+
DataStoreFactory.create(
62+
serializer = FakeAuthTokenSerializer,
63+
produceFile = { temporaryFolder.newFile("auth-token-test.enc") },
64+
)
65+
66+
authTokenDataStore = AuthTokenDataStoreImpl(dataStore)
67+
}
68+
69+
@Test
70+
fun `토큰 전체 업데이트가 성공하면 저장된 토큰을 반환해야 한다`() =
71+
runTest {
72+
// given
73+
val token =
74+
AuthToken(
75+
accessToken = "access",
76+
refreshToken = "refresh",
77+
)
78+
79+
// when
80+
val result = authTokenDataStore.updateAuthToken(token)
81+
82+
// then
83+
assertEquals(token, result)
84+
}
85+
86+
@Test
87+
fun `accessToken만 업데이트하면 기존 refreshToken은 유지되어야 한다`() =
88+
runTest {
89+
// given
90+
authTokenDataStore.updateAuthToken(
91+
AuthToken(
92+
accessToken = "oldAccess",
93+
refreshToken = "oldRefresh",
94+
),
95+
)
96+
97+
// when
98+
val updated = authTokenDataStore.updateAccessToken(accessToken = "newAccess")
99+
100+
// then
101+
assertEquals("newAccess", updated.accessToken)
102+
assertEquals("oldRefresh", updated.refreshToken)
103+
}
104+
105+
@Test
106+
fun `refreshToken만 업데이트하면 기존 accessToken은 유지되어야 한다`() =
107+
runTest {
108+
// given
109+
authTokenDataStore.updateAuthToken(
110+
AuthToken(
111+
accessToken = "oldAccess",
112+
refreshToken = "oldRefresh",
113+
),
114+
)
115+
116+
// when
117+
val updated = authTokenDataStore.updateRefreshToken(refreshToken = "newRefresh")
118+
119+
// then
120+
assertEquals("oldAccess", updated.accessToken)
121+
assertEquals("newRefresh", updated.refreshToken)
122+
}
123+
124+
@Test
125+
fun `토큰을 클리어하면 기본값이 저장되어야 한다`() =
126+
runTest {
127+
// given
128+
authTokenDataStore.updateAuthToken(
129+
AuthToken(
130+
accessToken = "someAccess",
131+
refreshToken = "someRefresh",
132+
),
133+
)
134+
135+
// when
136+
val cleared = authTokenDataStore.clearAuthToken()
137+
138+
// then
139+
assertEquals(AuthToken(), cleared)
140+
}
141+
142+
@Test
143+
fun `tokenFlow는 현재 저장된 토큰을 방출해야 한다`() =
144+
runTest {
145+
// given
146+
val token =
147+
AuthToken(
148+
accessToken = "flowAccess",
149+
refreshToken = "flowRefresh",
150+
)
151+
152+
// when
153+
authTokenDataStore.updateAuthToken(token)
154+
155+
// then
156+
val flowValue = authTokenDataStore.tokenFlow.first()
157+
assertEquals(token, flowValue)
158+
}
159+
160+
@Test(expected = RuntimeException::class)
161+
fun `updateAuthToken에서 예외 발생시 예외가 전파되어야 한다`() =
162+
runTest {
163+
// given
164+
val brokenStore =
165+
object : DataStore<AuthToken> {
166+
override val data = flowOf(AuthToken())
167+
168+
override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken {
169+
throw RuntimeException("updateAuthToken failed")
170+
}
171+
}
172+
val failingDataStore = AuthTokenDataStoreImpl(brokenStore)
173+
174+
// when & then
175+
failingDataStore.updateAuthToken(AuthToken("access", "refresh"))
176+
}
177+
178+
@Test(expected = RuntimeException::class)
179+
fun `updateAccessToken에서 예외 발생시 예외가 전파되어야 한다`() =
180+
runTest {
181+
// given
182+
val brokenStore =
183+
object : DataStore<AuthToken> {
184+
override val data = flowOf(AuthToken())
185+
186+
override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken {
187+
throw RuntimeException("updateAccessToken failed")
188+
}
189+
}
190+
val failingDataStore = AuthTokenDataStoreImpl(brokenStore)
191+
192+
// when & then
193+
failingDataStore.updateAccessToken("newAccess")
194+
}
195+
196+
@Test(expected = RuntimeException::class)
197+
fun `updateRefreshToken에서 예외 발생시 예외가 전파되어야 한다`() =
198+
runTest {
199+
// given
200+
val brokenStore =
201+
object : DataStore<AuthToken> {
202+
override val data = flowOf(AuthToken())
203+
204+
override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken {
205+
throw RuntimeException("updateRefreshToken failed")
206+
}
207+
}
208+
val failingDataStore = AuthTokenDataStoreImpl(brokenStore)
209+
210+
// when & then
211+
failingDataStore.updateRefreshToken("newRefresh")
212+
}
213+
214+
@Test(expected = RuntimeException::class)
215+
fun `clearAuthToken에서 예외 발생시 예외가 전파되어야 한다`() =
216+
runTest {
217+
// given
218+
val brokenStore =
219+
object : DataStore<AuthToken> {
220+
override val data = flowOf(AuthToken())
221+
222+
override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken {
223+
throw RuntimeException("clearAuthToken failed")
224+
}
225+
}
226+
val failingDataStore = AuthTokenDataStoreImpl(brokenStore)
227+
228+
// when & then
229+
failingDataStore.clearAuthToken()
230+
}
231+
}

0 commit comments

Comments
 (0)