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
19 changes: 16 additions & 3 deletions app/src/main/java/com/plainstudio/stackcasino/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.firestore
import com.plainstudio.stackcasino.util.IdGenerator
import com.plainstudio.stackcasino.util.SystemTimeProvider
import com.plainstudio.stackcasino.util.TimeProvider
import com.plainstudio.stackcasino.util.UuidIdGenerator
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -14,9 +18,10 @@ import javax.inject.Singleton
/**
* Application-scoped Hilt bindings.
*
* Currently exposes only the Firebase singletons. They are not consumed
* yet; the providers exist so downstream cards (Login, Wallet, History,
* etc.) inject the SDK without touching DI plumbing.
* Exposes the Firebase singletons plus the app-wide testability seams
* ([TimeProvider], [IdGenerator]). The Firebase providers exist so
* downstream cards (Login, Wallet, History, etc.) inject the SDK
* without touching DI plumbing.
*
* FirebaseApp is initialized implicitly by FirebaseInitProvider (which
* the google-services Gradle plugin registers in the merged manifest),
Expand All @@ -36,4 +41,12 @@ object AppModule {
@Provides
@Singleton
fun provideFirebaseFirestore(): FirebaseFirestore = Firebase.firestore

@Provides
@Singleton
fun provideTimeProvider(): TimeProvider = SystemTimeProvider()

@Provides
@Singleton
fun provideIdGenerator(): IdGenerator = UuidIdGenerator()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import androidx.lifecycle.viewModelScope
import com.plainstudio.stackcasino.domain.assistant.AssistantRepository
import com.plainstudio.stackcasino.domain.assistant.ChatTurn
import com.plainstudio.stackcasino.domain.assistant.Role
import com.plainstudio.stackcasino.util.IdGenerator
import com.plainstudio.stackcasino.util.TimeProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID
import javax.inject.Inject

/**
Expand All @@ -37,6 +37,8 @@ class AssistantViewModel
@Inject
constructor(
private val repository: AssistantRepository,
private val timeProvider: TimeProvider,
private val idGenerator: IdGenerator,
) : ViewModel() {
private val _uiState = MutableStateFlow<AssistantUiState>(AssistantUiState.Welcome)
val uiState: StateFlow<AssistantUiState> = _uiState.asStateFlow()
Expand Down Expand Up @@ -116,11 +118,11 @@ class AssistantViewModel
copy(messages = messages + message)
}

private fun nowLabel(): String = TIMESTAMP_FORMAT.format(Date())
private fun nowLabel(): String = TIMESTAMP_FORMAT.format(timeProvider.now())

// Random per-message handle so the LazyColumn key never collides
// on repeated identical questions sent inside the same minute.
private fun newId(): String = UUID.randomUUID().toString()
// Per-message handle so the LazyColumn key never collides on
// repeated identical questions sent inside the same minute.
private fun newId(): String = idGenerator.newId()

companion object {
const val MAX_USER_MESSAGE_LENGTH = 300
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/plainstudio/stackcasino/util/IdGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.plainstudio.stackcasino.util

import java.util.UUID

/**
* Seam over random id generation so classes that key list items by id
* stay deterministic under unit tests. Production code gets
* [UuidIdGenerator]; tests inject a fake that returns predictable ids.
*/
fun interface IdGenerator {
fun newId(): String
}

/**
* Default [IdGenerator] backed by random UUIDs.
*/
class UuidIdGenerator : IdGenerator {
override fun newId(): String = UUID.randomUUID().toString()
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/plainstudio/stackcasino/util/TimeProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.plainstudio.stackcasino.util

import java.util.Date

/**
* Seam over the system clock so time-dependent classes (chat message
* timestamps, round labels, ...) stay deterministic under unit tests.
* Production code gets [SystemTimeProvider]; tests inject a fake that
* returns a fixed instant.
*/
fun interface TimeProvider {
fun now(): Date
}

/**
* Default [TimeProvider] backed by the real wall clock.
*/
class SystemTimeProvider : TimeProvider {
override fun now(): Date = Date()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.plainstudio.stackcasino.domain.assistant.AssistantRepository
import com.plainstudio.stackcasino.domain.assistant.ChatTurn
import com.plainstudio.stackcasino.domain.assistant.Role
import com.plainstudio.stackcasino.testing.MainDispatcherRule
import com.plainstudio.stackcasino.util.IdGenerator
import com.plainstudio.stackcasino.util.TimeProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
Expand All @@ -15,14 +17,21 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import java.util.Date

class AssistantViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()

private val repository = mockk<AssistantRepository>()

private fun viewModel(): AssistantViewModel = AssistantViewModel(repository)
// Deterministic seams so message ids and timestamps stay stable
// across runs instead of leaning on UUID / the wall clock.
private val fixedTime = TimeProvider { Date(0L) }
private var idCounter = 0
private val sequentialIds = IdGenerator { "msg-${idCounter++}" }

private fun viewModel(): AssistantViewModel = AssistantViewModel(repository, fixedTime, sequentialIds)

// --- initial state ----------------------------------------------------

Expand Down
Loading