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
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,21 @@ class SettingImplementedCoverageAndroidTest {
composeRule.onAllNodes(hasText("프로필 수정")).assertCountEquals(0)
}

@Test
fun settingNavHost_passwordChangeEntryAndBackKeepSettingsHost() {
launchHost()
waitForSettingHomeContent()

composeRule.onNodeWithText("비밀번호 변경").performScrollTo().performClick()
composeRule.onNodeWithText("현재 비밀번호").assertIsDisplayed()
composeRule.onNodeWithText("새 비밀번호").assertIsDisplayed()
composeRule.onNodeWithContentDescription("뒤로가기").performClick()

waitForSettingHomeContent()
composeRule.onNodeWithText("프로필 수정").assertIsDisplayed()
assertEquals(0, composeRule.runOnIdle { exits })
}

@Test
fun settingNavHost_receiverManageListRowNavigatesToEditWithExactReceiverId() {
launchHost()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,5 @@ internal class AccountRepositoryImpl
newPassword,
),
).requireStatus()
}
}.mapAccountFailure()
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import org.junit.Test
* 번역해 Presentation 이 타입으로 분기(인라인 표시)할 수 있게 하고, 그 외 실패
* (네트워크·서버 오류 등)는 원본 예외를 유지해 기존 스낵바 경로로 흐르게 한다.
*
* 설정의 비밀번호 변경(#564)도 같은 규칙을 따른다 — 이 메서드만 번역이 빠져 있어 1206·1702 가
* 원본 `ApiException` 으로 새고 있었다. 화면이 타입으로만 분기하므로 번역이 끊기면 조용히 폴백
* 문구가 나간다.
*
* 비밀번호 찾기(#457)가 더한 두 코드도 같은 규칙을 따른다 — 소셜 가입 계정(1702)은 화면이
* 차단 팝업으로, 새 비밀번호가 기존과 같음(1206)은 스낵바 전용 문구로 가른다. 둘 다 타입으로만
* 갈리므로 여기서 번역이 끊기면 화면이 조용히 폴백 문구를 낸다.
Expand Down Expand Up @@ -182,8 +186,93 @@ class AccountRepositoryImplTest {
assertEquals(PasswordFindRequestDto(EMAIL, CODE, NEW_PASSWORD, "Confirm1!"), sent)
}

@Test
fun `passwordChange - 기존과 같은 비밀번호(1206)는 PasswordUnchanged 로 번역`() {
val repository =
repository(
FakeAccountApiService(
onPasswordChange = {
throw ApiException(
status = 400,
code = 1206,
serverMessage = "새 비밀번호와 같습니다.",
fallbackMessage = "새 비밀번호와 같습니다.",
)
},
),
)

val result = runBlocking { repository.passwordChange(CURRENT_PASSWORD, NEW_PASSWORD) }

assertTrue(result.exceptionOrNull() is CoreAuthFailure.PasswordUnchanged)
}

@Test
fun `passwordChange - 소셜 가입 계정(1702)은 SocialSignUpAccount 로 번역`() {
val repository =
repository(
FakeAccountApiService(
onPasswordChange = {
throw ApiException(
status = 400,
code = 1702,
serverMessage = "소셜 로그인으로 가입한 계정입니다. 소셜 로그인을 이용해주세요.",
fallbackMessage = "소셜 로그인으로 가입한 계정입니다.",
)
},
),
)

val result = runBlocking { repository.passwordChange(CURRENT_PASSWORD, NEW_PASSWORD) }

assertTrue(result.exceptionOrNull() is CoreAuthFailure.SocialSignUpAccount)
}

@Test
fun `passwordChange - 그 외 code 실패는 원본 ApiException 유지 (폴백 문구 경로)`() {
val repository =
repository(
FakeAccountApiService(
onPasswordChange = {
throw ApiException(
status = 401,
code = 1202,
serverMessage = null,
fallbackMessage = "아이디 또는 비밀번호가 일치하지 않습니다.",
)
},
),
)

val result = runBlocking { repository.passwordChange(CURRENT_PASSWORD, NEW_PASSWORD) }

val error = result.exceptionOrNull()
assertTrue(error is ApiException)
assertEquals(1202, (error as ApiException).code)
}

@Test
fun `passwordChange - 두 필드를 서버 요청 그대로 싣는다`() {
var sent: PasswordChangeRequestDto? = null
val repository =
repository(
FakeAccountApiService(
onPasswordChange = { body ->
sent = body
success(Unit)
},
),
)

val result = runBlocking { repository.passwordChange(CURRENT_PASSWORD, NEW_PASSWORD) }

assertTrue(result.isSuccess)
assertEquals(PasswordChangeRequestDto(CURRENT_PASSWORD, NEW_PASSWORD), sent)
}

private companion object {
const val EMAIL = "user@example.com"
const val CURRENT_PASSWORD = "OldPass1!"
const val CODE = "123456"
const val NEW_PASSWORD = "NewPass1!"
}
Expand All @@ -202,6 +291,9 @@ private class FakeAccountApiService(
private val onFindPassword: (PasswordFindRequestDto) -> BaseResponse<Unit> = {
error("findPassword 는 이 시나리오에서 호출되면 안 됨")
},
private val onPasswordChange: (PasswordChangeRequestDto) -> BaseResponse<Unit> = {
error("passwordChange 는 이 시나리오에서 호출되면 안 됨")
},
) : AccountApiService {
override suspend fun sendEmailCode(body: SendEmailCodeRequestDto): BaseResponse<Unit> = error("sendEmailCode 는 이 시나리오에서 호출되면 안 됨")

Expand All @@ -216,5 +308,5 @@ private class FakeAccountApiService(

override suspend fun signUp(body: SignUpRequestDto): BaseResponse<SignUpDto> = error("signUp 은 이 시나리오에서 호출되면 안 됨")

override suspend fun passwordChange(body: PasswordChangeRequestDto): BaseResponse<Unit> = error("passwordChange 는 이 시나리오에서 호출되면 안 됨")
override suspend fun passwordChange(body: PasswordChangeRequestDto): BaseResponse<Unit> = onPasswordChange(body)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ interface AccountRepository {
confirmPassword: String,
): Result<Unit>

/**
* 로그인한 사용자의 비밀번호 변경(서버 계약명은 `auth/password/change`).
*
* [resetPassword] 와 달리 인증번호를 쓰지 않고 현재 비밀번호로 본인을 확인한다. 확인이 끝난
* 계정이라 `confirmPassword` 계약 필드도 없다(BE `PasswordChangeRequest` 는 두 필드뿐이다).
*
* 사유가 확인되는 실패는 [com.afternote.core.domain.error.CoreAuthFailure] 로 번역된다 —
* 새 비밀번호가 기존과 같음(1206)은 `PasswordUnchanged`, 비밀번호가 없는 소셜 가입 계정(1702)은
* `SocialSignUpAccount`. **현재 비밀번호 불일치(1202)는 아직 번역되지 않아** 원본 예외로 온다.
*/
suspend fun passwordChange(
currentPassword: String,
newPassword: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal fun SettingScreen(
onBackClick: () -> Unit,
onLogoutSuccess: () -> Unit,
onProfileEditClick: () -> Unit,
onPasswordChangeClick: () -> Unit,
onLinkedAccountClick: () -> Unit,
onNotificationClick: () -> Unit,
onRecipientListClick: () -> Unit,
Expand Down Expand Up @@ -96,6 +97,7 @@ internal fun SettingScreen(
}
},
onProfileEditClick = onProfileEditClick,
onPasswordChangeClick = onPasswordChangeClick,
onLinkedAccountClick = onLinkedAccountClick,
onNotificationClick = onNotificationClick,
onRecipientListClick = onRecipientListClick,
Expand All @@ -116,6 +118,7 @@ private fun SettingScreenContent(
onLogoutClick: () -> Unit,
onUnavailableClick: () -> Unit,
onProfileEditClick: () -> Unit,
onPasswordChangeClick: () -> Unit,
onLinkedAccountClick: () -> Unit,
onNotificationClick: () -> Unit,
onRecipientListClick: () -> Unit,
Expand Down Expand Up @@ -175,7 +178,7 @@ private fun SettingScreenContent(
)
SettingMenuItem(
label = stringResource(R.string.setting_account_password_change),
onClick = onUnavailableClick,
onClick = onPasswordChangeClick,
)
SettingMenuItem(
label = stringResource(R.string.setting_account_linked_account),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ internal class SettingLocalNavActions(
backStack.add(SettingRoute.ProfileEditRoute)
}

override fun onPasswordChangeClick() {
backStack.add(SettingRoute.PasswordChangeRoute)
}

override fun onLinkedAccountClick() {
backStack.add(SettingRoute.LinkedAccountRoute)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal interface SettingNavActions {

fun onProfileEditClick()

fun onPasswordChangeClick()

fun onLinkedAccountClick()

fun onNotificationClick()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.afternote.feature.setting.presentation.passkey.PassKeyMakingScreen
import com.afternote.feature.setting.presentation.passkey.PassKeyPasswordScreen
import com.afternote.feature.setting.presentation.passkey.PassKeyScreen
import com.afternote.feature.setting.presentation.passkey.PassKeyViewModel
import com.afternote.feature.setting.presentation.password.PasswordChangeScreen
import com.afternote.feature.setting.presentation.profile.ProfileEditScreen
import com.afternote.feature.setting.presentation.receiver.ReceiverEditScreen
import com.afternote.feature.setting.presentation.receiver.ReceiverEditViewModel
Expand Down Expand Up @@ -97,6 +98,7 @@ public fun SettingNavHost(
onBackClick = actions::popBack,
onLogoutSuccess = actions::onLogoutSuccess,
onProfileEditClick = actions::onProfileEditClick,
onPasswordChangeClick = actions::onPasswordChangeClick,
onLinkedAccountClick = actions::onLinkedAccountClick,
onNotificationClick = actions::onNotificationClick,
onRecipientListClick = actions::onRecipientListClick,
Expand Down Expand Up @@ -138,6 +140,13 @@ public fun SettingNavHost(
)
}

entry<SettingRoute.PasswordChangeRoute> {
PasswordChangeScreen(
onBackClick = actions::popBack,
onChanged = actions::popBack,
)
}

entry<SettingRoute.LinkedAccountRoute> {
ConnectedAccountsScreen(
onBack = actions::popBack,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ internal sealed interface SettingRoute : NavKey {
@Serializable
data object ProfileEditRoute : SettingRoute

@Serializable
data object PasswordChangeRoute : SettingRoute

@Serializable
data object LinkedAccountRoute : SettingRoute

Expand Down
Loading
Loading