Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/java/com/kikidan/todakun/TodakunApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ fun TodakunApp(
navigator.push(TodakunRoute.NotificationSetting)
},
onNavigateToAppSetting = { navigator.push(TodakunRoute.AppSetting) },
onNavigateToLogin = { navigator.resetTo(TodakunRoute.Login) },
snackbarHostState = snackbarHostState,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ class AuthRepositoryImpl
runCatchingCancellable {
remoteAuthDataSource.postSignup(signupSubmission, onboardingToken)
}

override suspend fun logout(): Result<Unit> =
runCatchingCancellable {
remoteAuthDataSource.postLogout()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class FakeRemoteAuthDataSource : RemoteAuthDataSource {
var refreshResult: AuthToken = AuthToken("new-access", "new-refresh")
var throwOnRefresh: Throwable? = null

var throwOnLogout: Throwable? = null
var logoutCalled = false

override suspend fun postLogin(oauthCredential: OAuthCredential): LoginResult {
throwOnLogin?.let { throw it }
return loginResult
Expand All @@ -31,7 +34,10 @@ class FakeRemoteAuthDataSource : RemoteAuthDataSource {
return signupResult
}

override suspend fun postLogout() = error("not used")
override suspend fun postLogout() {
logoutCalled = true
throwOnLogout?.let { throw it }
}

override suspend fun postRefresh(refreshToken: String): AuthToken {
throwOnRefresh?.let { throw it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,39 @@ class AuthRepositoryImplTest {
// when
sut.signup(user, onboardingToken)
}

@Test
fun `logout이_성공하면_DataSource의_postLogout이_호출되고_Result_success가_반환된다`() =
runTest {
// when
val result = sut.logout()

// then
assertTrue(fakeRemoteAuthDataSource.logoutCalled)
assertTrue(result.isSuccess)
}

@Test
fun `DataSource의_logout이_예외를_throw하면_Result_failure로_반환되고_예외가_누수되지_않는다`() =
runTest {
// given
fakeRemoteAuthDataSource.throwOnLogout = IOException("network error")

// when
val result = sut.logout()

// then
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is IOException)
}

@Test(expected = CancellationException::class)
fun `DataSource의_logout이_CancellationException을_throw하면_Result로_감싸지지_않고_그대로_전파된다`() =
runTest {
// given
fakeRemoteAuthDataSource.throwOnLogout = CancellationException("cancelled")

// when
sut.logout()
}
}
2 changes: 2 additions & 0 deletions core/designsystem/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
<string name="mypage_menu_app_setting">앱 설정</string>
<string name="mypage_menu_inquiry">1:1 문의</string>
<string name="mypage_menu_logout">로그아웃</string>
<string name="mypage_inquiry_email_error">메일 앱을 찾을 수 없어요.</string>
<string name="mypage_logout_error">알 수 없는 에러가 발생했어요. 잠시 후 다시 시도해주세요</string>
<string name="mypage_app_version_label">앱 버전</string>
<string name="mypage_app_version_format">v %1$s</string>
<string name="mypage_view_mansaeryeok_button">만세력 보기</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ enum class OnboardingTerm(
) {
SERVICE(
required = true,
link = "https://ash-topaz-463.notion.site/3b081c6748468045a408eb20d27e2342?source=copy_link",
link = "https://ash-topaz-463.notion.site/3b081c67484680aca6e5ec1d463c670d?source=copy_link",
),
PRIVACY(
required = true,
link = "https://ash-topaz-463.notion.site/3b081c67484680aca6e5ec1d463c670d?source=copy_link",
link = "https://ash-topaz-463.notion.site/3b081c6748468045a408eb20d27e2342?source=copy_link",
),
AI_DATA_TRANSFER(
required = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ interface AuthRepository {
): Result<AuthToken>

suspend fun refresh(refreshToken: String): Result<AuthToken>

suspend fun logout(): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.kikidan.domain.usecase

import com.kikidan.domain.repository.AuthRepository
import com.kikidan.domain.repository.TokenRepository
import javax.inject.Inject

class LogoutUseCase
@Inject
constructor(
private val authRepository: AuthRepository,
private val tokenRepository: TokenRepository,
) {
// 서버 로그아웃 요청이 네트워크 등으로 실패해도 로컬 토큰은 항상 제거해 기기에서는 로그아웃 상태로 만든다.
suspend operator fun invoke(): Result<Unit> {
authRepository.logout()
return tokenRepository.clearToken()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.kikidan.domain.fake

import com.kikidan.domain.model.auth.AuthToken
import com.kikidan.domain.model.auth.LoginResult
import com.kikidan.domain.model.auth.OAuthCredential
import com.kikidan.domain.model.auth.OnboardingToken
import com.kikidan.domain.model.auth.SignupSubmission
import com.kikidan.domain.repository.AuthRepository

class FakeAuthRepository : AuthRepository {
var loginResult: Result<LoginResult> = Result.failure(IllegalStateException("not used"))
var signupResult: Result<AuthToken> = Result.failure(IllegalStateException("not used"))
var refreshResult: Result<AuthToken> = Result.failure(IllegalStateException("not used"))
var logoutResult: Result<Unit> = Result.success(Unit)
var logoutCalled = false

override suspend fun login(credential: OAuthCredential): Result<LoginResult> = loginResult

override suspend fun signup(
signupSubmission: SignupSubmission,
onboardingToken: OnboardingToken,
): Result<AuthToken> = signupResult

override suspend fun refresh(refreshToken: String): Result<AuthToken> = refreshResult

override suspend fun logout(): Result<Unit> {
logoutCalled = true
return logoutResult
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.kikidan.domain.fake

import com.kikidan.domain.model.auth.AuthToken
import com.kikidan.domain.repository.TokenRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

class FakeTokenRepository : TokenRepository {
var loginStateResult: Result<Boolean> = Result.success(false)
var getTokenResult: Result<AuthToken?> = Result.success(null)
var saveTokenResult: Result<Unit> = Result.success(Unit)
var clearTokenResult: Result<Unit> = Result.success(Unit)
var clearTokenCalled = false

override fun observeLoginState(): Flow<Result<Boolean>> = flowOf(loginStateResult)

override suspend fun getToken(): Result<AuthToken?> = getTokenResult

override suspend fun saveToken(token: AuthToken): Result<Unit> = saveTokenResult

override suspend fun clearToken(): Result<Unit> {
clearTokenCalled = true
return clearTokenResult
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.kikidan.domain.usecase

import com.kikidan.domain.fake.FakeAuthRepository
import com.kikidan.domain.fake.FakeTokenRepository
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class LogoutUseCaseTest {
private lateinit var fakeAuthRepository: FakeAuthRepository
private lateinit var fakeTokenRepository: FakeTokenRepository
private lateinit var useCase: LogoutUseCase

@Before
fun setUp() {
fakeAuthRepository = FakeAuthRepository()
fakeTokenRepository = FakeTokenRepository()
useCase = LogoutUseCase(fakeAuthRepository, fakeTokenRepository)
}

@Test
fun `성공 시 서버 로그아웃과 로컬 토큰 삭제가 모두 호출되고 Result success를 반환한다`() =
runTest {
val result = useCase()

assertTrue(fakeAuthRepository.logoutCalled)
assertTrue(fakeTokenRepository.clearTokenCalled)
assertTrue(result.isSuccess)
}

@Test
fun `서버 로그아웃이 실패해도 로컬 토큰은 삭제되고 그 결과를 반환한다`() =
runTest {
fakeAuthRepository.logoutResult = Result.failure(IllegalStateException("network"))

val result = useCase()

assertTrue(fakeAuthRepository.logoutCalled)
assertTrue(fakeTokenRepository.clearTokenCalled)
assertTrue(result.isSuccess)
}

@Test
fun `로컬 토큰 삭제가 실패하면 Result failure를 그대로 전파한다`() =
runTest {
val error = IllegalStateException("disk error")
fakeTokenRepository.clearTokenResult = Result.failure(error)

val result = useCase()

assertTrue(result.isFailure)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ class SignUpUseCaseTest {
): Result<AuthToken> = signupResult

override suspend fun refresh(refreshToken: String): Result<AuthToken> = error("not used")

override suspend fun logout(): Result<Unit> = error("not used")
}

private class FakeTokenRepository : TokenRepository {
Expand Down
Loading
Loading