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
@@ -1,37 +1,22 @@
package nl.jovmit.androiddevs.domain.auth

import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import nl.jovmit.androiddevs.domain.auth.data.AuthResult
import nl.jovmit.androiddevs.domain.auth.data.User
import javax.inject.Inject
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class AuthModule {
object AuthModule {

@Binds
@Singleton
internal abstract fun bindAuthRepository(
repository: DummyAuthRepo
): AuthRepository
@Provides
@Singleton
internal fun bindAuthRepository(): AuthRepository =
InMemoryAuthRepository()

@Binds
@Singleton
internal abstract fun bindUserSession(
userSession: InMemoryUserSession
): UserSession

class DummyAuthRepo @Inject constructor() : AuthRepository {
override suspend fun login(email: String, password: String): AuthResult {
return AuthResult.Success("token", User("userId", email, "about"))
}

override suspend fun signUp(email: String, password: String, about: String): AuthResult {
return AuthResult.Success("token", User("userId", email, about))
}
}
@Provides
@Singleton
internal fun bindUserSession(): UserSession =
InMemoryUserSession()
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class InMemoryAuthRepository(
override suspend fun login(email: String, password: String): AuthResult {
if (isUnavailable) return AuthResult.BackendError
if (isOffline) return AuthResult.OfflineError

val matchingUsers = _usersForPassword.getOrElse(password) { emptyList() }
val found = matchingUsers.find { it.email == email }
found?.let { user ->
Expand All @@ -33,6 +34,7 @@ class InMemoryAuthRepository(
if (isUnavailable) return AuthResult.BackendError
if (isOffline) return AuthResult.OfflineError
if (isKnownUser(email)) return AuthResult.ExistingUserError

val user = User(UUID.randomUUID().toString(), email, about)
saveUserData(password, user)
return AuthResult.Success(authToken, user)
Expand All @@ -44,10 +46,9 @@ class InMemoryAuthRepository(

private fun saveUserData(password: String, user: User) {
val currentUsers = _usersForPassword.getOrElse(password) { emptyList() }
currentUsers.toMutableList().apply {
_usersForPassword[password] = currentUsers.toMutableList().apply {
add(user)
}
_usersForPassword[password] = currentUsers
}

fun setLoggedInUsers(usersForPassword: Map<String, List<User>>) {
Expand All @@ -61,4 +62,4 @@ class InMemoryAuthRepository(
fun setOffline() {
isOffline = true
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
package nl.jovmit.androiddevs.base.auth

import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import nl.jovmit.androiddevs.domain.auth.AuthRepository
import nl.jovmit.androiddevs.domain.auth.InMemoryAuthRepository
import nl.jovmit.androiddevs.domain.auth.data.AuthResult
import nl.jovmit.androiddevs.domain.auth.data.User
import org.junit.jupiter.api.Test

class InMemoryAuthTest : AuthContractTest() {

@Test
fun signedUpUsersHaveDistinctIdentities() = runTest {
val repository = InMemoryAuthRepository()

val alice = repository.signUp("alice@androiddevs.nl", "passWord12.", "Compose mentor")
val bob = repository.signUp("bob@androiddevs.nl", "passWord12.", "Architecture coach")

assertThat((alice as AuthResult.Success).user.userId)
.isNotEqualTo((bob as AuthResult.Success).user.userId)
}

@Test
fun signedUpUsersCanLogInWithTheirCredentials() = runTest {
val repository = InMemoryAuthRepository()
val signedUp = repository.signUp("alice@androiddevs.nl", "passWord12.", "Compose mentor")

val loggedIn = repository.login("alice@androiddevs.nl", "passWord12.")

assertThat(loggedIn).isEqualTo(signedUp)
}

override fun authRepositoryWith(
authToken: String,
usersForPassword: Map<String, List<User>>
Expand All @@ -20,4 +45,4 @@ class InMemoryAuthTest : AuthContractTest() {
override fun offlineAuthRepository(): AuthRepository {
return InMemoryAuthRepository().apply { setOffline() }
}
}
}
1 change: 1 addition & 0 deletions feature/postdetails/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
kapt(libs.hilt.compiler)

testImplementation(libs.bundles.unit.testing)
testImplementation(projects.testutils)

testRuntimeOnly(libs.junit.jupiter.engine)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.jovmit.androiddevs.domain.auth.UserSession
import nl.jovmit.androiddevs.domain.auth.data.User
import nl.jovmit.androiddevs.domain.timeline.PostDetailsResult
import nl.jovmit.androiddevs.domain.timeline.RemovePostResult
import nl.jovmit.androiddevs.domain.timeline.TimelineRepository
Expand All @@ -27,6 +30,12 @@ class PostDetailsViewModel @Inject constructor(

val screenState = _screenState.asStateFlow()

init {
userSession.sessionUser
.onEach(::onSessionUserChanged)
.launchIn(viewModelScope)
}

fun loadPostDetails(postId: String) {
viewModelScope.launch {
setLoading()
Expand Down Expand Up @@ -59,7 +68,7 @@ class PostDetailsViewModel @Inject constructor(
_screenState.update {
when (result) {
is PostDetailsResult.Success -> PostDetailsScreenState(
postItem = result.post.toPostDetailsItem()
postItem = result.post.toPostDetailsItem(userSession.sessionUser.value)
)
PostDetailsResult.PostNotFound -> PostDetailsScreenState(isNotFound = true)
PostDetailsResult.Offline -> PostDetailsScreenState(isOffline = true)
Expand All @@ -68,10 +77,17 @@ class PostDetailsViewModel @Inject constructor(
}
}

private fun Post.toPostDetailsItem(): PostDetailsItem {
private fun onSessionUserChanged(sessionUser: User?) {
_screenState.update { screenState ->
val post = screenState.postItem?.post ?: return@update screenState
screenState.copy(postItem = post.toPostDetailsItem(sessionUser))
}
}

private fun Post.toPostDetailsItem(sessionUser: User?): PostDetailsItem {
return PostDetailsItem(
post = this,
canRemove = userSession.sessionUser.value?.userId == author.userId
canRemove = sessionUser?.userId == author.userId
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package nl.jovmit.androiddevs.feature.postdetails

import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import nl.jovmit.androiddevs.domain.auth.InMemoryAuthRepository
import nl.jovmit.androiddevs.domain.auth.InMemoryUserSession
import nl.jovmit.androiddevs.domain.auth.data.AuthResult
import nl.jovmit.androiddevs.domain.timeline.AddPostResult
import nl.jovmit.androiddevs.domain.timeline.InMemoryTimelineRepository
import nl.jovmit.androiddevs.domain.timeline.RemovePostResult
import nl.jovmit.androiddevs.testutils.CoroutineTestExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CoroutineTestExtension::class)
class AccountSwitchPostOwnershipRegressionTest {

@Test
fun userCannotRemoveAnotherUsersPostAfterAccountSwitch() = runTest {
val authRepository = InMemoryAuthRepository()
val session = InMemoryUserSession()
val timelineRepository = InMemoryTimelineRepository(session)
val alice = authRepository.signUp(
email = "alice@androiddevs.nl",
password = "passWord12.",
about = "Compose mentor"
).authenticatedUser()
session.setSessionUser(alice)
val alicePost = (timelineRepository.addPost(
title = "How I debug recomposition spikes",
body = "I stopped guessing and started measuring recomposition from the user-facing screen."
) as AddPostResult.Success).post

session.clear()
authRepository.signUp(
email = "bob@androiddevs.nl",
password = "passWord12.",
about = "Architecture coach"
)
val bob = authRepository.login("bob@androiddevs.nl", "passWord12.").authenticatedUser()
session.setSessionUser(bob)
val viewModel = PostDetailsViewModel(
timelineRepository = timelineRepository,
userSession = session,
backgroundDispatcher = Dispatchers.Unconfined
)

viewModel.loadPostDetails(alicePost.id)

assertThat(viewModel.screenState.value.postItem?.post?.id).isEqualTo(alicePost.id)
assertThat(viewModel.screenState.value.postItem?.canRemove).isFalse()
assertThat(timelineRepository.removePost(alicePost.id)).isEqualTo(RemovePostResult.NotAuthor)
}

private fun AuthResult.authenticatedUser() = (this as AuthResult.Success).user
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package nl.jovmit.androiddevs.feature.postdetails

import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import nl.jovmit.androiddevs.domain.auth.InMemoryUserSession
import nl.jovmit.androiddevs.domain.auth.data.User
import nl.jovmit.androiddevs.domain.timeline.AddPostResult
import nl.jovmit.androiddevs.domain.timeline.PostDetailsResult
import nl.jovmit.androiddevs.domain.timeline.RemovePostResult
import nl.jovmit.androiddevs.domain.timeline.TimelineRepository
import nl.jovmit.androiddevs.domain.timeline.TimelineResult
import nl.jovmit.androiddevs.domain.timeline.data.Post
import nl.jovmit.androiddevs.testutils.CoroutineTestExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CoroutineTestExtension::class)
class PostDetailsViewModelTest {

private val maya = User("maya", "maya@androiddevs.nl", "Compose mentor")
private val sam = User("sam", "sam@androiddevs.nl", "Architecture coach")
private val post = Post(
id = "post-compose",
author = maya,
title = "How I debug recomposition spikes",
body = "I stopped guessing and started measuring recomposition from the user-facing screen.",
createdAtMillis = 1_700_000_000_000L
)
private val session = InMemoryUserSession()
private val timelineRepository = FakeTimelineRepository(post)

@Test
fun deleteVisibilityFollowsActiveSessionUser() = runTest {
session.setSessionUser(maya)
val viewModel = PostDetailsViewModel(
timelineRepository = timelineRepository,
userSession = session,
backgroundDispatcher = Dispatchers.Unconfined
)

viewModel.loadPostDetails(post.id)

assertThat(viewModel.screenState.value.postItem?.canRemove).isTrue()

session.setSessionUser(sam)

assertThat(viewModel.screenState.value.postItem?.canRemove).isFalse()
}

@Test
fun deleteHiddenWhenActiveSessionUserIsNotTheAuthor() = runTest {
session.setSessionUser(sam)
val viewModel = PostDetailsViewModel(
timelineRepository = timelineRepository,
userSession = session,
backgroundDispatcher = Dispatchers.Unconfined
)

viewModel.loadPostDetails(post.id)

assertThat(viewModel.screenState.value.postItem?.canRemove).isFalse()
}

private class FakeTimelineRepository(
private val post: Post
) : TimelineRepository {

override val timeline: Flow<TimelineResult> = MutableStateFlow(TimelineResult.Success(emptyList()))

override suspend fun getPost(postId: String): PostDetailsResult {
return if (postId == post.id) {
PostDetailsResult.Success(post)
} else {
PostDetailsResult.PostNotFound
}
}

override suspend fun addPost(title: String, body: String): AddPostResult {
error("Not needed for this test")
}

override suspend fun removePost(postId: String): RemovePostResult {
error("Not needed for this test")
}
}
}
Loading