Skip to content
Open
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
@@ -0,0 +1,176 @@
package com.afternote.core.domain.usecase.auth

import com.afternote.core.domain.repository.auth.AuthRepository
import com.afternote.core.model.Session
import com.afternote.core.model.TokenBundle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* [LoginUseCase] 비즈니스 로직 회귀 가드.
*
* 검증 핵심:
* 1. 로그인 타입(Email/Kakao/Google)에 맞는 [AuthRepository] 메서드로 분기하는지
* 2. 로그인 성공 시 받은 세션 토큰을 그대로 [AuthRepository.saveSession]에 전달하는지
* 3. 로그인 실패면 **saveSession을 호출하지 않고** 그 실패를 그대로 반환(short-circuit)하는지
* 4. saveSession 실패면 그 실패를 반환하는지
* 5. 반환값(신규 가입자 여부) — 소셜 `newUser=true` 만 true, 이메일·기존(false)·null 은 false
*
* 외부 라이브러리(mockk 등) 없이 호출 인자/횟수를 기록하는 직접 작성 fake를 사용한다.
*/
class LoginUseCaseTest {
@Test
fun `Email 로그인 성공 - defaultLogin 호출 후 세션 토큰으로 saveSession`() {
val repo =
FakeAuthRepository().apply {
defaultLoginResult = Result.success(Session.DefaultSession(accessToken = "AT", refreshToken = "RT"))
}
val result = runBlocking { LoginUseCase(repo)(LoginType.Email(email = "a@b.com", password = "pw")) }

assertTrue(result.isSuccess)
assertFalse(result.getOrThrow()) // 이메일 로그인 = 기존 유저(false)
assertEquals("a@b.com" to "pw", repo.defaultLoginArgs)
assertEquals("AT" to "RT", repo.saveSessionArgs)
assertEquals(1, repo.saveSessionCallCount)
}

@Test
fun `Kakao 로그인 성공 - kakaoLogin 호출 + saveSession`() {
val repo =
FakeAuthRepository().apply {
kakaoResult =
Result.success(Session.SocialSession(accessToken = "KAT", refreshToken = "KRT", isNewUser = true))
}
val result = runBlocking { LoginUseCase(repo)(LoginType.Kakao(oauthToken = "kakao-token")) }

assertTrue(result.isSuccess)
assertTrue(result.getOrThrow()) // isNewUser=true → 신규
assertEquals("kakao-token", repo.kakaoArg)
assertEquals("KAT" to "KRT", repo.saveSessionArgs)
}

@Test
fun `Google 로그인 성공 - googleLogin 호출 + saveSession`() {
val repo =
FakeAuthRepository().apply {
googleResult =
Result.success(Session.SocialSession(accessToken = "GAT", refreshToken = "GRT", isNewUser = false))
}
val result = runBlocking { LoginUseCase(repo)(LoginType.Google(idToken = "google-id-token")) }

assertTrue(result.isSuccess)
assertFalse(result.getOrThrow()) // isNewUser=false → 기존
assertEquals("google-id-token", repo.googleArg)
assertEquals("GAT" to "GRT", repo.saveSessionArgs)
}

@Test
fun `소셜 로그인 - isNewUser null 이면 false (기존 유저 취급)`() {
val repo =
FakeAuthRepository().apply {
kakaoResult =
Result.success(Session.SocialSession(accessToken = "KAT", refreshToken = "KRT", isNewUser = null))
}
val result = runBlocking { LoginUseCase(repo)(LoginType.Kakao(oauthToken = "kakao-token")) }

assertTrue(result.isSuccess)
assertFalse(result.getOrThrow()) // null → false (`== true` 가 null 흡수)
}

@Test
fun `로그인 실패면 saveSession 호출하지 않고 실패를 그대로 반환`() {
val loginError = IllegalStateException("login failed")
val repo =
FakeAuthRepository().apply {
defaultLoginResult = Result.failure(loginError)
}
val result = runBlocking { LoginUseCase(repo)(LoginType.Email(email = "a@b.com", password = "pw")) }

assertTrue(result.isFailure)
assertSame(loginError, result.exceptionOrNull())
assertEquals(0, repo.saveSessionCallCount)
assertNull(repo.saveSessionArgs)
}

@Test
fun `saveSession 실패면 그 실패를 반환`() {
val saveError = IllegalStateException("save failed")
val repo =
FakeAuthRepository().apply {
defaultLoginResult = Result.success(Session.DefaultSession(accessToken = "AT", refreshToken = "RT"))
saveSessionResult = Result.failure(saveError)
}
val result = runBlocking { LoginUseCase(repo)(LoginType.Email(email = "a@b.com", password = "pw")) }

assertFalse(result.isSuccess)
assertSame(saveError, result.exceptionOrNull())
assertEquals(1, repo.saveSessionCallCount)
}

private class FakeAuthRepository : AuthRepository {
var defaultLoginArgs: Pair<String, String>? = null
var kakaoArg: String? = null
var googleArg: String? = null
var saveSessionArgs: Pair<String, String>? = null
var saveSessionCallCount = 0

var defaultLoginResult: Result<Session.DefaultSession> =
Result.success(Session.DefaultSession(accessToken = "at", refreshToken = "rt"))
var kakaoResult: Result<Session.SocialSession> =
Result.success(Session.SocialSession(accessToken = "at", refreshToken = "rt", isNewUser = false))
var googleResult: Result<Session.SocialSession> =
Result.success(Session.SocialSession(accessToken = "at", refreshToken = "rt", isNewUser = false))
var saveSessionResult: Result<Unit> = Result.success(Unit)

override val isLoggedIn: Flow<Boolean> = flowOf(false)

override suspend fun saveSession(
accessToken: String,
refreshToken: String,
): Result<Unit> {
saveSessionCallCount++
saveSessionArgs = accessToken to refreshToken
return saveSessionResult
}

override suspend fun updateTokens(
accessToken: String,
refreshToken: String,
): Result<Unit> = Result.success(Unit)

override suspend fun clearSession(): Result<Unit> = Result.success(Unit)

override suspend fun getAccessToken(): Result<String?> = Result.success(null)

override suspend fun getRefreshToken(): Result<String?> = Result.success(null)

override suspend fun defaultLogin(
email: String,
password: String,
): Result<Session.DefaultSession> {
defaultLoginArgs = email to password
return defaultLoginResult
}

override suspend fun kakaoLogin(oauthToken: String): Result<Session.SocialSession> {
kakaoArg = oauthToken
return kakaoResult
}

override suspend fun googleLogin(idToken: String): Result<Session.SocialSession> {
googleArg = idToken
return googleResult
}

override suspend fun rotateToken(): Result<TokenBundle> = Result.success(TokenBundle(accessToken = "at", refreshToken = "rt"))

override suspend fun logout(): Result<Unit> = Result.success(Unit)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.afternote.feature.afternote.domain.usecase.editor

import com.afternote.feature.afternote.domain.repository.author.MediaInput
import com.afternote.feature.afternote.domain.repository.author.MemorialPhotoUploadRepository
import com.afternote.feature.afternote.domain.repository.author.MemorialVideoUploadRepository
import com.afternote.feature.afternote.domain.repository.author.PhotoUploadOutcome
import com.afternote.feature.afternote.domain.repository.author.VideoUploadOutcome
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* [ResolveMemorialMediaForSaveUseCase] 비즈니스 로직 회귀 가드.
*
* 검증 핵심:
* 1. 입력 [MediaInput] 을 각 Repository 에 그대로 전달하는지 (로컬/원격 확정은 호출부 책임)
* 2. sealed [VideoUploadOutcome]/[PhotoUploadOutcome] 분기를 저장 페이로드 URL 로 매핑하는지
* (Empty→null, Existing→url, FreshlyUploaded→url)
* 3. 영상 resolve 실패면 [MemorialVideoSaveException] 으로 wrap 하고 **사진 Repository 는 호출하지 않은 채**
* short-circuit 하는지 (non-local return)
* 4. 사진 resolve 실패면 [MemorialPhotoSaveException] 으로 wrap 하고, 두 경우 모두 원본 예외를 cause 로 보존하는지
*
* 외부 라이브러리(mockk 등) 없이 호출 인자/횟수를 기록하는 직접 작성 fake를 사용한다.
*/
class ResolveMemorialMediaForSaveUseCaseTest {
@Test
fun `영상-사진 모두 성공 - FreshlyUploaded 영상과 Existing 사진을 각 URL 로 매핑`() {
val videoRepo = FakeVideoUploadRepository().apply { result = Result.success(VideoUploadOutcome.FreshlyUploaded("v-url")) }
val photoRepo = FakePhotoUploadRepository().apply { result = Result.success(PhotoUploadOutcome.Existing("p-url")) }

val result =
runBlocking {
ResolveMemorialMediaForSaveUseCase(videoRepo, photoRepo)(
video = MediaInput.Local("content://video"),
photo = MediaInput.Remote("https://cdn/photo.jpg"),
)
}

assertTrue(result.isSuccess)
assertEquals("v-url", result.getOrThrow().resolvedVideoUrl)
assertEquals("p-url", result.getOrThrow().resolvedMemorialPhotoUrl)
}

@Test
fun `Empty Outcome 은 null 로 매핑 - 미첨부`() {
val videoRepo = FakeVideoUploadRepository().apply { result = Result.success(VideoUploadOutcome.Empty) }
val photoRepo = FakePhotoUploadRepository().apply { result = Result.success(PhotoUploadOutcome.Empty) }

val resolved =
runBlocking {
ResolveMemorialMediaForSaveUseCase(videoRepo, photoRepo)(MediaInput.None, MediaInput.None)
}.getOrThrow()

assertNull(resolved.resolvedVideoUrl)
assertNull(resolved.resolvedMemorialPhotoUrl)
}

@Test
fun `Existing 영상과 FreshlyUploaded 사진도 각 URL 로 매핑`() {
val videoRepo = FakeVideoUploadRepository().apply { result = Result.success(VideoUploadOutcome.Existing("existing-v")) }
val photoRepo = FakePhotoUploadRepository().apply { result = Result.success(PhotoUploadOutcome.FreshlyUploaded("fresh-p")) }

val resolved =
runBlocking {
ResolveMemorialMediaForSaveUseCase(videoRepo, photoRepo)(
video = MediaInput.Remote("https://cdn/existing-v.mp4"),
photo = MediaInput.Local("content://fresh-photo"),
)
}.getOrThrow()

assertEquals("existing-v", resolved.resolvedVideoUrl)
assertEquals("fresh-p", resolved.resolvedMemorialPhotoUrl)
}

@Test
fun `입력 MediaInput 을 각 Repository 에 그대로 전달`() {
val videoRepo = FakeVideoUploadRepository()
val photoRepo = FakePhotoUploadRepository()

val videoInput = MediaInput.Local("content://fv")
val photoInput = MediaInput.Remote("https://cdn/mp.jpg")
runBlocking {
ResolveMemorialMediaForSaveUseCase(videoRepo, photoRepo)(videoInput, photoInput)
}

assertEquals(videoInput, videoRepo.resolveVideoArg)
assertEquals(photoInput, photoRepo.resolvePhotoArg)
}

@Test
fun `영상 resolve 실패면 MemorialVideoSaveException 으로 wrap 하고 사진 Repository 는 호출하지 않음`() {
val videoError = IllegalStateException("video resolve failed")
val videoRepo = FakeVideoUploadRepository().apply { result = Result.failure(videoError) }
val photoRepo = FakePhotoUploadRepository()

val result =
runBlocking {
ResolveMemorialMediaForSaveUseCase(videoRepo, photoRepo)(MediaInput.Local("content://v"), MediaInput.None)
}

assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is MemorialVideoSaveException)
assertSame(videoError, result.exceptionOrNull()?.cause) // 원본 예외를 cause 로 보존
assertEquals(0, photoRepo.callCount) // short-circuit — 사진 Repository 미호출
}

@Test
fun `사진 resolve 실패면 MemorialPhotoSaveException 으로 wrap`() {
val photoError = IllegalStateException("photo resolve failed")
val videoRepo = FakeVideoUploadRepository().apply { result = Result.success(VideoUploadOutcome.Empty) }
val photoRepo = FakePhotoUploadRepository().apply { result = Result.failure(photoError) }

val result =
runBlocking {
ResolveMemorialMediaForSaveUseCase(videoRepo, photoRepo)(MediaInput.None, MediaInput.Remote("https://cdn/p.jpg"))
}

assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is MemorialPhotoSaveException)
assertSame(photoError, result.exceptionOrNull()?.cause)
}

private class FakeVideoUploadRepository : MemorialVideoUploadRepository {
var resolveVideoArg: MediaInput? = null
var callCount = 0
var result: Result<VideoUploadOutcome> = Result.success(VideoUploadOutcome.Empty)

override suspend fun resolveVideo(input: MediaInput): Result<VideoUploadOutcome> {
callCount++
resolveVideoArg = input
return result
}
}

private class FakePhotoUploadRepository : MemorialPhotoUploadRepository {
var resolvePhotoArg: MediaInput? = null
var callCount = 0
var result: Result<PhotoUploadOutcome> = Result.success(PhotoUploadOutcome.Empty)

override suspend fun resolvePhoto(input: MediaInput): Result<PhotoUploadOutcome> {
callCount++
resolvePhotoArg = input
return result
}
}
}
Loading