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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ dependencies {
implementation(projects.core.dataLocal)
implementation(projects.core.navigation)
implementation(projects.core.designsystem)
implementation(projects.feature.onboarding)
implementation(projects.feature.auth)

implementation(libs.androidx.navigation3.runtime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.kikidan.data.datasource.RemoteAuthDataSource
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
import com.kikidan.domain.util.runCatchingCancellable
import javax.inject.Inject
Expand All @@ -22,4 +24,12 @@ class AuthRepositoryImpl
runCatchingCancellable {
remoteAuthDataSource.postRefresh(refreshToken)
}

override suspend fun signup(
signupSubmission: SignupSubmission,
onboardingToken: OnboardingToken,
): Result<AuthToken> =
runCatchingCancellable {
remoteAuthDataSource.postSignup(signupSubmission, onboardingToken)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class FakeRemoteAuthDataSource : RemoteAuthDataSource {
var loginResult: LoginResult =
LoginResult(AuthToken("access", "refresh"), OnboardingToken("onboarding"), newMember = false)
var throwOnLogin: Throwable? = null
var signupResult: AuthToken = AuthToken("signup-access", "signup-refresh")
var throwOnSignup: Throwable? = null

var refreshResult: AuthToken = AuthToken("new-access", "new-refresh")
var throwOnRefresh: Throwable? = null
Expand All @@ -24,7 +26,10 @@ class FakeRemoteAuthDataSource : RemoteAuthDataSource {
override suspend fun postSignup(
signupSubmission: SignupSubmission,
onboardingToken: OnboardingToken,
): AuthToken = error("not used")
): AuthToken {
throwOnSignup?.let { throw it }
return signupResult
}

override suspend fun postLogout() = error("not used")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,41 @@ package com.kikidan.data.repository

import com.kikidan.data.fake.FakeRemoteAuthDataSource
import com.kikidan.domain.model.auth.AuthToken
import com.kikidan.domain.model.auth.Job
import com.kikidan.domain.model.auth.LoginResult
import com.kikidan.domain.model.auth.OAuthCredential
import com.kikidan.domain.model.auth.OAuthProviderType
import com.kikidan.domain.model.auth.OAuthToken
import com.kikidan.domain.model.auth.OnboardingToken
import com.kikidan.domain.model.auth.RelationshipStatus
import com.kikidan.domain.model.auth.SignupSubmission
import com.kikidan.domain.model.user.Birth
import com.kikidan.domain.model.user.BirthTime
import com.kikidan.domain.model.user.DateType
import com.kikidan.domain.model.user.Gender
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.IOException
import java.time.LocalDate

class AuthRepositoryImplTest {
private lateinit var fakeRemoteAuthDataSource: FakeRemoteAuthDataSource
private lateinit var sut: AuthRepositoryImpl

private val credential = OAuthCredential(OAuthProviderType.KAKAO, OAuthToken("token"))
private val user =
SignupSubmission(
name = "토닥이",
job = Job.STUDENT,
relationshipStatus = RelationshipStatus.SOLO,
gender = Gender.FEMALE,
birth = Birth(DateType.SOLAR, LocalDate.of(1999, 2, 13), BirthTime.JA),
)
private val onboardingToken = OnboardingToken("onboarding-1")

@Before
fun setUp() {
Expand Down Expand Up @@ -92,4 +109,42 @@ class AuthRepositoryImplTest {
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is IOException)
}

@Test
fun `signup이_성공하면_DataSource의_AuthToken이_그대로_Result_success로_반환된다`() =
runTest {
// given
val expected = AuthToken("a-2", "r-2")
fakeRemoteAuthDataSource.signupResult = expected

// when
val result = sut.signup(user, onboardingToken)

// then
assertEquals(expected, result.getOrNull())
}

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

// when
val result = sut.signup(user, onboardingToken)

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

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

// when
sut.signup(user, onboardingToken)
}
}
2 changes: 2 additions & 0 deletions core/domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ dependencies {
implementation(libs.javax.inject)
implementation(libs.kotlinx.serialization.json)
api(libs.kotlinx.coroutines.core)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kikidan.domain.model.onboarding

enum class OnboardingTerm(
val required: Boolean,
) {
SERVICE(required = true),
PRIVACY(required = true),
AI_DATA_TRANSFER(required = true),
MARKETING(required = false),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.kikidan.domain.model.onboarding

/**
* 이름 입력 검증 결과. 실패 사유별로 화면이 다른 안내 문구를 고를 수 있도록 타입으로 구분한다.
*/
sealed interface UserName {
data class Valid(
val name: String,
) : UserName

sealed interface Invalid : UserName {
data object Empty : Invalid

data class TooLong(
val input: String,
) : Invalid

data class ContainsSpecialCharacter(
val input: String,
) : Invalid
}

companion object {
const val MAX_LENGTH = 10
private val ALLOWED_PATTERN = Regex("^[가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9 ]+$")

fun from(raw: String): UserName {
val trimmed = raw.trim()
return when {
trimmed.isEmpty() -> Invalid.Empty
trimmed.length > MAX_LENGTH -> Invalid.TooLong(raw)
!ALLOWED_PATTERN.matches(trimmed) -> Invalid.ContainsSpecialCharacter(raw)
else -> Valid(trimmed)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ package com.kikidan.domain.repository
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

interface AuthRepository {
suspend fun login(credential: OAuthCredential): Result<LoginResult>

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

suspend fun refresh(refreshToken: String): Result<AuthToken>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.kikidan.domain.usecase

import com.kikidan.domain.model.auth.AuthToken
import com.kikidan.domain.model.auth.OnboardingToken
import com.kikidan.domain.model.auth.SignupSubmission
import com.kikidan.domain.repository.AuthRepository
import com.kikidan.domain.repository.TokenRepository
import javax.inject.Inject

class SignUpUseCase
@Inject
constructor(
private val authRepository: AuthRepository,
private val tokenRepository: TokenRepository,
) {
suspend operator fun invoke(
signupSubmission: SignupSubmission,
onboardingToken: OnboardingToken,
): Result<AuthToken> {
val authToken =
authRepository.signup(signupSubmission, onboardingToken).getOrElse { return Result.failure(it) }
tokenRepository.saveToken(authToken).getOrElse { return Result.failure(it) }
return Result.success(authToken)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.kikidan.domain.model.onboarding

import org.junit.Assert.assertEquals
import org.junit.Test

class UserNameTest {
@Test
fun `한글 이름은 통과한다`() {
assertEquals(UserName.Valid("토닥이"), UserName.from("토닥이"))
}

@Test
fun `영문과 숫자, 공백이 섞여도 통과한다`() {
assertEquals(UserName.Valid("Todak un 2"), UserName.from("Todak un 2"))
}

@Test
fun `빈 문자열은 Empty다`() {
assertEquals(UserName.Invalid.Empty, UserName.from(""))
}

@Test
fun `공백만 있으면 Empty다`() {
assertEquals(UserName.Invalid.Empty, UserName.from(" "))
}

@Test
fun `특수문자가 포함되면 ContainsSpecialCharacter다`() {
assertEquals(UserName.Invalid.ContainsSpecialCharacter("토닥이##"), UserName.from("토닥이##"))
}

@Test
fun `이모지도 특수문자로 걸러진다`() {
assertEquals(UserName.Invalid.ContainsSpecialCharacter("토닥이🙂"), UserName.from("토닥이🙂"))
}

@Test
fun `최대 길이까지는 통과한다`() {
val name = "가".repeat(UserName.MAX_LENGTH)

assertEquals(UserName.Valid(name), UserName.from(name))
}

@Test
fun `최대 길이를 넘으면 TooLong이다`() {
val name = "가".repeat(UserName.MAX_LENGTH + 1)

assertEquals(UserName.Invalid.TooLong(name), UserName.from(name))
}

@Test
fun `앞뒤 공백은 길이 계산에서 제외된다`() {
val trimmed = "가".repeat(UserName.MAX_LENGTH)
val name = " $trimmed "

assertEquals(UserName.Valid(trimmed), UserName.from(name))
}
}
Loading
Loading