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: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
kotlin.code.style=official
# Determines whether this project should use hardcoded mocks or Supabase-CLI.
# See https://github.com/PPartisan/Roky/wiki/Project-Setup
USE_LOCAL_MOCKS=false
USE_LOCAL_MOCKS=true
2 changes: 2 additions & 0 deletions src/main/kotlin/chatserver/ChatServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package chatserver

import chatserver.MessagesRepository.Read
import chatserver.MessagesRepository.Write
import chatserver.profiles.chatServerProfilesModule
import org.koin.dsl.binds
import org.koin.dsl.module

val chatServerModule =
module {
includes(chatServerProfilesModule)
factory { MockMessages } binds arrayOf(Read::class, Write::class)
}
31 changes: 31 additions & 0 deletions src/main/kotlin/chatserver/profiles/ChatServerProfiles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package chatserver.profiles

import arch.RokyDispatchers
import chatserver.ProfileResult
import chatserver.ReadChatRepository
import chatserver.SubscribeChatRepository
import chatserver.WriteChatRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.koin.dsl.module

val chatServerProfilesModule =
module {
single {
LocalProfilesRepository(
get<RokyDispatchers>(),
CoroutineScope(
SupervisorJob() + get<RokyDispatchers>().default,
),
)
}
factory<ReadChatRepository<ProfileResult>> {
get<LocalProfilesRepository>()
}
factory<WriteChatRepository<String>> {
get<LocalProfilesRepository>()
}
factory<SubscribeChatRepository> {
get<LocalProfilesRepository>()
}
}
66 changes: 66 additions & 0 deletions src/main/kotlin/chatserver/profiles/LocalProfilesRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package chatserver.profiles

import arch.RokyDispatchers
import chatroom.viewmessages.ViewMessagesUseCase
import chatserver.ProfileResult
import chatserver.ProfileResult.Companion.ok
import chatserver.ReadChatRepository
import chatserver.SubscribeChatRepository
import chatserver.WriteChatRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds

class LocalProfilesRepository(
private val dispatchers: RokyDispatchers,
private val scope: CoroutineScope,
) : ReadChatRepository<ProfileResult>, WriteChatRepository<String>, SubscribeChatRepository {
private val state: MutableStateFlow<ProfileResult> = MutableStateFlow(ok(emptyMap()))

override fun latest(): ProfileResult {
return state.value
}

override fun observe(): Flow<ProfileResult> {
return state.asStateFlow()
}

override fun subscribe() {
scope.launch(dispatchers.default) {
while (true) {
val users = ViewMessagesUseCase.sampleUsers.shuffled()
state.value = users.associateWith { it }.let(ProfileResult::ok)
delay(5.seconds)
}
}
}

override fun unsubscribe() {
// deliberately empty
}

override fun write(item: String) {
scope.launch {
try {
require(item.isNotBlank()) { "Username cannot be empty." }
delay(2.seconds)
check(item.isValidUsername()) { "Could not assign current username." }
val profiles = latest().item.toMutableMap()
profiles[item] = item
state.value = ok(profiles)
} catch (e: Exception) {
state.value = ProfileResult.fail(e)
}
}
}

companion object {
private const val VALID_LENGTH = 6

private fun String.isValidUsername(): Boolean = length < VALID_LENGTH
}
}
3 changes: 1 addition & 2 deletions src/main/kotlin/profile/Profile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ val profileModules =
ProfilePresenter(
windowScope = get<ProfileWindow>().windowScope,
requestUsername = get(),
usernames = get(),
dispatchers = get(),
)
}
scoped { RequestUserNameUseCase(get()) }
}
factory { RequestUserNameUseCase.RequestUserName() }
}
29 changes: 25 additions & 4 deletions src/main/kotlin/profile/ProfilePresenter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ package profile

import arch.Presenter
import arch.RokyDispatchers
import chatserver.ProfileResult
import chatserver.ReadChatRepository
import chatserver.WriteChatRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
import profile.ProfileEvent.RequestUsername
import profile.ProfileViewState.Idle
import profile.ProfileViewState.Pending
import profile.ProfileViewState.*

class ProfilePresenter(
private val windowScope: CoroutineScope,
private val requestUsername: RequestUserNameUseCase,
private val requestUsername: WriteChatRepository<String>,
private val usernames: ReadChatRepository<ProfileResult>,
dispatchers: RokyDispatchers,
) : Presenter<ProfileView>(dispatchers) {
private val state: MutableStateFlow<ProfileViewState> = MutableStateFlow(Idle)
Expand All @@ -20,6 +26,9 @@ class ProfilePresenter(
windowScope.launch(dispatchers.main) {
state.collect(::show)
}
windowScope.launch(dispatchers.io) {
usernames.observe().drop(1).map { it.toViewState() }.collect { state.value = it }
}
}

override fun onDetach(view: ProfileView) {
Expand All @@ -35,11 +44,23 @@ class ProfilePresenter(
private fun onRequestUsername(event: RequestUsername) {
state.value = Pending
windowScope.launch(dispatchers.io) {
state.value = requestUsername(event.username)
requestUsername.write(event.username)
}
}

private fun show(viewState: ProfileViewState) {
withView { view -> view.show(viewState) }
}

private fun ProfileResult.toViewState(): ProfileViewState =
if (isOk) {
Success(MESSAGE_OK)
} else {
Failed(error?.message ?: "Unsuccessful!")
}

companion object {
@VisibleForTesting
internal const val MESSAGE_OK = "Successfully changed username!"
}
}
38 changes: 0 additions & 38 deletions src/main/kotlin/profile/RequestUserNameUseCase.kt

This file was deleted.

44 changes: 32 additions & 12 deletions src/test/kotlin/profile/ProfilePresenterTest.kt
Original file line number Diff line number Diff line change
@@ -1,44 +1,65 @@
package profile

import arch.RokyDispatchers
import coAnswersDelayed
import io.mockk.*
import chatserver.ProfileResult
import chatserver.ReadChatRepository
import chatserver.WriteChatRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifyOrder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import profile.ProfilePresenter.Companion.MESSAGE_OK
import profile.ProfileViewState.*

@OptIn(ExperimentalCoroutinesApi::class)
class ProfilePresenterTest {
private lateinit var scope: CoroutineScope
private lateinit var view: ProfileView
private lateinit var requestUsername: RequestUserNameUseCase
private lateinit var usernames: MutableStateFlow<ProfileResult>
private lateinit var presenter: ProfilePresenter

@BeforeEach
fun setUp() {
requestUsername = mockk()
coEvery { requestUsername(any()) } coAnswersDelayed { Idle }

view = mockk(relaxed = true)
val dispatchers: RokyDispatchers =
mockk<RokyDispatchers>().apply {
every { main } returns dispatcher
every { io } returns dispatcher
}
usernames = MutableStateFlow(ProfileResult.ok(emptyMap()))
val usernameRepository: ReadChatRepository<ProfileResult> = mockk()
every { usernameRepository.observe() } returns usernames
every { usernameRepository.latest() } answers {
usernames.value
}
val writeUsername: WriteChatRepository<String> = mockk()
every { writeUsername.write(any()) } answers {
val user = it.invocation.args [0] as String
usernames.value =
if (user == INVALID_USER) {
ProfileResult.fail(RuntimeException())
} else {
val currentUsernames = usernames.value.item.toMutableMap()
currentUsernames[user] = user
ProfileResult.ok(currentUsernames)
}
}
scope = CoroutineScope(dispatcher)
presenter = ProfilePresenter(scope, requestUsername, dispatchers)
presenter = ProfilePresenter(scope, writeUsername, usernameRepository, dispatchers)
}

@Test
fun `given user requests new usernames, when request is successful, then show success status`() =
runTest(dispatcher) {
coEvery { requestUsername(any()) } coAnswersDelayed { Success("OKAYYY") }
presenter.attach(view)
advanceUntilIdle()
presenter.onEvent(ProfileEvent.RequestUsername("USERNAMEE"))
Expand All @@ -49,7 +70,7 @@ class ProfilePresenterTest {
view.show(
withArg {
assertTrue(it is Success)
assertTrue(it.status == "OKAYYY")
assertTrue(it.status == MESSAGE_OK)
},
)
}
Expand All @@ -58,18 +79,16 @@ class ProfilePresenterTest {
@Test
fun `given user requests new username, when request fails, then show failed status`() =
runTest(dispatcher) {
coEvery { requestUsername(any()) } coAnswersDelayed { Failed("!OKAYYY") }
presenter.attach(view)
advanceUntilIdle()
presenter.onEvent(ProfileEvent.RequestUsername("USERNAMEE"))
presenter.onEvent(ProfileEvent.RequestUsername(INVALID_USER))
advanceUntilIdle()
verifyOrder {
view.show(Idle)
view.show(Pending)
view.show(
withArg {
assertTrue(it is Failed)
assertTrue(it.status == "!OKAYYY")
},
)
}
Expand All @@ -85,5 +104,6 @@ class ProfilePresenterTest {

companion object {
private val dispatcher = StandardTestDispatcher()
private const val INVALID_USER = "INVALID USER!"
}
}
Loading
Loading