diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index da84614..0fd441e 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -2,7 +2,6 @@ - ComplexCondition:MainActivity.kt$!isOnWelcomeScreen && !isOnMessagingScreen && !isOnAnnounceDetailScreen && !isOnInterfaceManagementScreen && !isOnBleConnectionStatusScreen && !isOnThemeManagementScreen && !isOnThemeEditorScreen && !isOnRNodeWizardScreen CyclomaticComplexMethod:ColumbaApplication.kt$ColumbaApplication$override fun onCreate() CyclomaticComplexMethod:ConfigFileParser.kt$ConfigFileParser$private fun parseConfigLines(lines: List<String>): List<InterfaceConfig> CyclomaticComplexMethod:DebugViewModel.kt$DebugViewModel$private fun fetchDebugInfo() @@ -16,20 +15,20 @@ ImplicitDefaultLocale:ThemeColorGenerator.kt$ThemeColorGenerator$String.format("#%06X", argb and 0xFFFFFF) ImplicitDefaultLocale:ThemeColorGenerator.kt$ThemeColorGenerator$String.format("#%08X", argb) InstanceOfCheckForException:ServiceNotificationManager.kt$ServiceNotificationManager$e is android.app.ForegroundServiceStartNotAllowedException + LargeClass:MessagingViewModel.kt$MessagingViewModel : ViewModel + LargeClass:ReticulumServiceBinder.kt$ReticulumServiceBinder : Stub LargeClass:ServiceReticulumProtocol.kt$ServiceReticulumProtocol : ReticulumProtocol LongMethod:ColumbaApplication.kt$ColumbaApplication$override fun onCreate() LongMethod:ConfigFileParser.kt$ConfigFileParser$private fun parseConfigLines(lines: List<String>): List<InterfaceConfig> LongMethod:InterfaceRepository.kt$InterfaceRepository$fun entityToConfig(entity: InterfaceEntity): InterfaceConfig LongMethod:MessageCollector.kt$MessageCollector$fun startCollecting() - LongMethod:MessagingViewModel.kt$MessagingViewModel$fun sendMessage( destinationHash: String, content: String, ) LongParameterList:BleTestFixtures.kt$BleTestFixtures$( identityHash: String = "abcd1234efgh5678ijkl9012mnop3456", peerName: String = "RNS-TestPeer", currentMac: String = "AA:BB:CC:DD:EE:FF", connectionType: ConnectionType = ConnectionType.BOTH, rssi: Int = -70, mtu: Int = 512, connectedAt: Long = System.currentTimeMillis() - 60000, // 1 minute ago firstSeen: Long = System.currentTimeMillis() - 300000, // 5 minutes ago lastSeen: Long = System.currentTimeMillis(), bytesReceived: Long = 1024, bytesSent: Long = 2048, packetsReceived: Long = 10, packetsSent: Long = 20, successRate: Double = 1.0, ) + LongParameterList:MarkerBitmapFactory.kt$MarkerBitmapFactory$( sizeDp: Float = 28f, fillColor: Int, strokeColor: Int, fillOpacity: Float = 0.6f, strokeWidthDp: Float = 3f, dashLengthDp: Float = 4f, gapLengthDp: Float = 4f, density: Float, ) LongParameterList:ReticulumServiceBinder.kt$ReticulumServiceBinder$( private val context: Context, private val state: ServiceState, private val wrapperManager: PythonWrapperManager, private val identityManager: IdentityManager, private val routingManager: RoutingManager, private val messagingManager: MessagingManager, private val pollingManager: PollingManager, private val broadcaster: CallbackBroadcaster, private val lockManager: LockManager, private val maintenanceManager: MaintenanceManager, private val notificationManager: ServiceNotificationManager, private val bleCoordinator: BleCoordinator, private val scope: CoroutineScope, private val onInitialized: () -> Unit, private val onShutdown: () -> Unit, private val onForceExit: () -> Unit, ) LoopWithTooManyJumpStatements:ConfigFileParser.kt$ConfigFileParser$for LoopWithTooManyJumpStatements:RNodeWizardViewModel.kt$RNodeWizardViewModel$while LoopWithTooManyJumpStatements:SettingsViewModel.kt$SettingsViewModel$while MaxLineLength:BleTestFixtures.kt$BleTestFixtures$"""{"identityHash":"${connection.identityHash}","peerName":"${connection.peerName}","currentMac":"${connection.currentMac}","hasCentralConnection":$hasCentral,"hasPeripheralConnection":$hasPeripheral,"mtu":${connection.mtu},"connectedAt":${connection.connectedAt},"firstSeen":${connection.firstSeen},"lastSeen":${connection.lastSeen},"rssi":${connection.rssi}}""" - MaxLineLength:MainActivity.kt$// Only show NavigationBar when NOT on messaging screen, announce detail screen, interface management screen, BLE connection status screen, theme screens, welcome screen, or RNode wizard - MaxLineLength:MainActivity.kt$if MaxLineLength:SettingsViewModel.kt$SettingsViewModel$"Settings updated: displayName=${newState.displayName}, autoAnnounce=${newState.autoAnnounceEnabled}, interval=${newState.autoAnnounceIntervalMinutes}min, theme=${newState.selectedTheme}, customThemes=${newState.customThemes.size}" NestedBlockDepth:ConfigFileParser.kt$ConfigFileParser$private fun parseConfigLines(lines: List<String>): List<InterfaceConfig> NestedBlockDepth:SettingsViewModel.kt$SettingsViewModel$private suspend fun loadIdentityInfo(): Pair<String?, String?> @@ -45,6 +44,7 @@ SwallowedException:IdentityManagerViewModel.kt$IdentityManagerViewModel$e: Exception SwallowedException:IdentityQrCodeUtils.kt$IdentityQrCodeUtils$e: Exception SwallowedException:InterfaceConfigManager.kt$InterfaceConfigManager$e: Exception + SwallowedException:LocationSharingCard.kt$e: IllegalArgumentException SwallowedException:NodeTypeDetector.kt$NodeTypeDetector$e: Exception SwallowedException:NotificationHelper.kt$NotificationHelper$e: SecurityException SwallowedException:QrCodeComposables.kt$e: Exception @@ -68,7 +68,6 @@ TooGenericExceptionThrown:InterfaceConfigManager.kt$InterfaceConfigManager$throw Exception("Failed to initialize Reticulum: ${error.message}", error) TooGenericExceptionThrown:ReticulumServiceErrorHandlingTest.kt$ReticulumServiceErrorHandlingTest$throw RuntimeException("Real error") TooGenericExceptionThrown:ServiceReticulumProtocol.kt$ServiceReticulumProtocol$throw RuntimeException( "Missing identity fields in response. hash=$hashStr, publicKey=$publicKeyStr, privateKey=$privateKeyStr", ) - TooGenericExceptionThrown:ServiceReticulumProtocol.kt$ServiceReticulumProtocol$throw RuntimeException("Failed to restore peer identities: $error") TooGenericExceptionThrown:ServiceReticulumProtocol.kt$ServiceReticulumProtocol$throw RuntimeException("Service entered ERROR state: $status") TooGenericExceptionThrown:ServiceReticulumProtocol.kt$ServiceReticulumProtocol$throw RuntimeException("Timeout waiting for service to become READY (status: ${networkStatus.value})") TooGenericExceptionThrown:ServiceReticulumProtocol.kt$ServiceReticulumProtocol$throw RuntimeException(error) @@ -76,6 +75,7 @@ TooGenericExceptionThrown:ServiceReticulumProtocol.kt$ServiceReticulumProtocol$throw RuntimeException(result.optString("error", "Unknown error")) TooManyFunctions:InputValidator.kt$InputValidator TooManyFunctions:InterfaceManagementViewModel.kt$InterfaceManagementViewModel : ViewModel + TooManyFunctions:LocationSharingManager.kt$LocationSharingManager TooManyFunctions:RNodeWizardViewModel.kt$RNodeWizardViewModel : ViewModel TooManyFunctions:ReticulumServiceBinder.kt$ReticulumServiceBinder : Stub TooManyFunctions:ServiceReticulumProtocol.kt$ServiceReticulumProtocol : ReticulumProtocol @@ -94,8 +94,6 @@ UnusedPrivateMember:MessagingViewModel.kt$MessagingViewModel$private fun DataMessage.toReticulumMessage() UnusedPrivateProperty:DebugViewModel.kt$DebugViewModel$private val settingsRepository: SettingsRepository UnusedPrivateProperty:DebugViewModel.kt$DebugViewModel$val destHash = _publicKey.value ?: return null - UnusedPrivateProperty:DebugViewModel.kt$DebugViewModel$val isReady = status is com.lxmf.messenger.reticulum.model.NetworkStatus.READY - UnusedPrivateProperty:DebugViewModel.kt$DebugViewModel$val statusString = when (status) { is com.lxmf.messenger.reticulum.model.NetworkStatus.READY -> "READY" is com.lxmf.messenger.reticulum.model.NetworkStatus.INITIALIZING -> "INITIALIZING" is com.lxmf.messenger.reticulum.model.NetworkStatus.SHUTDOWN -> "SHUTDOWN" is com.lxmf.messenger.reticulum.model.NetworkStatus.ERROR -> "ERROR: ${status.message}" else -> status.toString() } UnusedPrivateProperty:DebugViewModel.kt$DebugViewModel.Companion$private const val DEFAULT_IDENTITY_FILE = "default_identity" UnusedPrivateProperty:IdentityScreen.kt$val context = LocalContext.current UnusedPrivateProperty:InterfaceConfigManager.kt$InterfaceConfigManager$val serviceConnection = (reticulumProtocol as ServiceReticulumProtocol) diff --git a/app/src/main/java/com/lxmf/messenger/viewmodel/MapViewModel.kt b/app/src/main/java/com/lxmf/messenger/viewmodel/MapViewModel.kt index 7079c5c..282a5c1 100644 --- a/app/src/main/java/com/lxmf/messenger/viewmodel/MapViewModel.kt +++ b/app/src/main/java/com/lxmf/messenger/viewmodel/MapViewModel.kt @@ -95,6 +95,13 @@ class MapViewModel private const val STALE_THRESHOLD_MS = 5 * 60 * 1000L // 5 minutes private const val GRACE_PERIOD_MS = 60 * 60 * 1000L // 1 hour private const val REFRESH_INTERVAL_MS = 30_000L // 30 seconds + + /** + * Controls whether periodic refresh is enabled. + * Set to false in tests to prevent infinite coroutine loops. + * @suppress VisibleForTesting + */ + internal var enablePeriodicRefresh = true } private val _state = MutableStateFlow(MapState()) @@ -196,10 +203,13 @@ class MapViewModel // Periodic refresh to update stale states (every 30 seconds) // This ensures markers transition from FRESH -> STALE as time passes - viewModelScope.launch { - while (isActive) { - delay(REFRESH_INTERVAL_MS) - _refreshTrigger.value = System.currentTimeMillis() + // Can be disabled for tests via enablePeriodicRefresh companion property + if (enablePeriodicRefresh) { + viewModelScope.launch { + while (isActive) { + delay(REFRESH_INTERVAL_MS) + _refreshTrigger.value = System.currentTimeMillis() + } } } } diff --git a/app/src/test/java/com/lxmf/messenger/ui/screens/ContactsScreenTest.kt b/app/src/test/java/com/lxmf/messenger/ui/screens/ContactsScreenTest.kt index 05752b8..344bc38 100644 --- a/app/src/test/java/com/lxmf/messenger/ui/screens/ContactsScreenTest.kt +++ b/app/src/test/java/com/lxmf/messenger/ui/screens/ContactsScreenTest.kt @@ -15,6 +15,7 @@ import com.lxmf.messenger.service.RelayInfo import com.lxmf.messenger.test.RegisterComponentActivityRule import com.lxmf.messenger.test.TestFactories import com.lxmf.messenger.viewmodel.AddContactResult +import com.lxmf.messenger.viewmodel.AnnounceStreamViewModel import com.lxmf.messenger.viewmodel.ContactGroups import com.lxmf.messenger.viewmodel.ContactsViewModel import io.mockk.coEvery @@ -85,7 +86,7 @@ class ContactsScreenTest { ) composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithText("No contacts yet").assertIsDisplayed() @@ -98,7 +99,7 @@ class ContactsScreenTest { val mockViewModel = createMockContactsViewModel() composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithText("Contacts").assertIsDisplayed() @@ -117,7 +118,7 @@ class ContactsScreenTest { ) composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithText("1 contact").assertIsDisplayed() @@ -138,7 +139,7 @@ class ContactsScreenTest { ) composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithText("3 contacts").assertIsDisplayed() @@ -149,7 +150,7 @@ class ContactsScreenTest { val mockViewModel = createMockContactsViewModel() composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithContentDescription("Search").assertIsDisplayed() @@ -161,7 +162,7 @@ class ContactsScreenTest { // val mockViewModel = createMockContactsViewModel() // // composeTestRule.setContent { - // ContactsScreen(viewModel = mockViewModel) + // ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) // } // // composeTestRule.onNodeWithContentDescription("Add contact").assertIsDisplayed() @@ -174,7 +175,7 @@ class ContactsScreenTest { val mockViewModel = createMockContactsViewModel() composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } // Initially search bar is hidden @@ -192,7 +193,7 @@ class ContactsScreenTest { val mockViewModel = createMockContactsViewModel() composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } // Open search bar @@ -211,7 +212,7 @@ class ContactsScreenTest { val mockViewModel = createMockContactsViewModel() composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } // Open search bar @@ -229,7 +230,7 @@ class ContactsScreenTest { val mockViewModel = createMockContactsViewModel(searchQuery = "Test") composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } // Open search bar @@ -261,7 +262,7 @@ class ContactsScreenTest { ) composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithText("MY RELAY").assertIsDisplayed() @@ -284,7 +285,7 @@ class ContactsScreenTest { ) composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithText("(auto)").assertIsDisplayed() @@ -306,7 +307,7 @@ class ContactsScreenTest { ) composeTestRule.setContent { - ContactsScreen(viewModel = mockViewModel) + ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) } composeTestRule.onNodeWithText("PINNED").assertIsDisplayed() @@ -339,7 +340,7 @@ class ContactsScreenTest { // ) // // composeTestRule.setContent { - // ContactsScreen(viewModel = mockViewModel) + // ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) // } // // // When there are pinned contacts, "ALL CONTACTS" header is shown @@ -366,7 +367,7 @@ class ContactsScreenTest { // ) // // composeTestRule.setContent { - // ContactsScreen(viewModel = mockViewModel) + // ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) // } // // composeTestRule.onNodeWithText("MY RELAY").assertIsDisplayed() @@ -1122,7 +1123,7 @@ class ContactsScreenTest { // val mockViewModel = createMockContactsViewModel() // // composeTestRule.setContent { - // ContactsScreen(viewModel = mockViewModel) + // ContactsScreen(viewModel = mockViewModel, announceViewModel = createMockAnnounceStreamViewModel()) // } // // // Click add button @@ -1152,6 +1153,7 @@ class ContactsScreenTest { composeTestRule.setContent { ContactsScreen( viewModel = mockViewModel, + announceViewModel = createMockAnnounceStreamViewModel(), onContactClick = { hash, name -> clickedHash = hash clickedName = name @@ -1191,4 +1193,12 @@ class ContactsScreenTest { return mockViewModel } + + private fun createMockAnnounceStreamViewModel(): AnnounceStreamViewModel { + val mock = mockk(relaxed = true) + every { mock.isAnnouncing } returns MutableStateFlow(false) + every { mock.announceSuccess } returns MutableStateFlow(false) + every { mock.announceError } returns MutableStateFlow(null) + return mock + } } diff --git a/app/src/test/java/com/lxmf/messenger/ui/screens/MessagingScreenTest.kt b/app/src/test/java/com/lxmf/messenger/ui/screens/MessagingScreenTest.kt index 6316f38..72f1991 100644 --- a/app/src/test/java/com/lxmf/messenger/ui/screens/MessagingScreenTest.kt +++ b/app/src/test/java/com/lxmf/messenger/ui/screens/MessagingScreenTest.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.test.performTextInput import androidx.paging.PagingData import com.lxmf.messenger.test.MessagingTestFixtures import com.lxmf.messenger.test.RegisterComponentActivityRule +import com.lxmf.messenger.ui.model.LocationSharingState import com.lxmf.messenger.viewmodel.ContactToggleResult import com.lxmf.messenger.viewmodel.MessagingViewModel import io.mockk.every @@ -66,6 +67,9 @@ class MessagingScreenTest { every { mockViewModel.totalAttachmentSize } returns MutableStateFlow(0) every { mockViewModel.fileAttachmentError } returns MutableSharedFlow() every { mockViewModel.isProcessingFile } returns MutableStateFlow(false) + // Location sharing mocks + every { mockViewModel.contacts } returns MutableStateFlow(emptyList()) + every { mockViewModel.locationSharingState } returns MutableStateFlow(LocationSharingState.NONE) } // ========== Empty State Tests ========== diff --git a/app/src/test/java/com/lxmf/messenger/viewmodel/MapViewModelTest.kt b/app/src/test/java/com/lxmf/messenger/viewmodel/MapViewModelTest.kt index f508cc7..0cd6187 100644 --- a/app/src/test/java/com/lxmf/messenger/viewmodel/MapViewModelTest.kt +++ b/app/src/test/java/com/lxmf/messenger/viewmodel/MapViewModelTest.kt @@ -55,13 +55,16 @@ class MapViewModelTest { @Before fun setup() { Dispatchers.setMain(testDispatcher) + // Disable periodic refresh to prevent infinite loops in tests + MapViewModel.enablePeriodicRefresh = false + contactRepository = mockk(relaxed = true) receivedLocationDao = mockk(relaxed = true) locationSharingManager = mockk(relaxed = true) announceDao = mockk(relaxed = true) every { contactRepository.getEnrichedContacts() } returns flowOf(emptyList()) - every { receivedLocationDao.getLatestLocationsPerSender(any()) } returns flowOf(emptyList()) + every { receivedLocationDao.getLatestLocationsPerSenderUnfiltered() } returns flowOf(emptyList()) every { receivedLocationDao.getLatestLocationsPerSenderUnfiltered() } returns flowOf(emptyList()) every { announceDao.getAllAnnounces() } returns flowOf(emptyList()) every { locationSharingManager.isSharing } returns MutableStateFlow(false) @@ -71,6 +74,8 @@ class MapViewModelTest { @After fun tearDown() { Dispatchers.resetMain() + // Re-enable periodic refresh for other tests + MapViewModel.enablePeriodicRefresh = true clearAllMocks() } @@ -204,7 +209,7 @@ class MapViewModelTest { ), ) every { contactRepository.getEnrichedContacts() } returns flowOf(contacts) - every { receivedLocationDao.getLatestLocationsPerSender(any()) } returns flowOf(receivedLocations) + every { receivedLocationDao.getLatestLocationsPerSenderUnfiltered() } returns flowOf(receivedLocations) viewModel = MapViewModel(contactRepository, receivedLocationDao, locationSharingManager, announceDao) @@ -238,7 +243,7 @@ class MapViewModelTest { ), ) every { contactRepository.getEnrichedContacts() } returns flowOf(contacts) - every { receivedLocationDao.getLatestLocationsPerSender(any()) } returns flowOf(receivedLocations) + every { receivedLocationDao.getLatestLocationsPerSenderUnfiltered() } returns flowOf(receivedLocations) viewModel = MapViewModel(contactRepository, receivedLocationDao, locationSharingManager, announceDao) @@ -314,7 +319,7 @@ class MapViewModelTest { ), ) every { contactRepository.getEnrichedContacts() } returns flowOf(contacts) - every { receivedLocationDao.getLatestLocationsPerSender(any()) } returns flowOf(receivedLocations) + every { receivedLocationDao.getLatestLocationsPerSenderUnfiltered() } returns flowOf(receivedLocations) viewModel = MapViewModel(contactRepository, receivedLocationDao, locationSharingManager, announceDao) val newLocation = createMockLocation(40.7128, -74.0060) // New York @@ -353,7 +358,7 @@ class MapViewModelTest { ) } every { contactRepository.getEnrichedContacts() } returns flowOf(contacts) - every { receivedLocationDao.getLatestLocationsPerSender(any()) } returns flowOf(receivedLocations) + every { receivedLocationDao.getLatestLocationsPerSenderUnfiltered() } returns flowOf(receivedLocations) viewModel = MapViewModel(contactRepository, receivedLocationDao, locationSharingManager, announceDao) @@ -384,7 +389,7 @@ class MapViewModelTest { ) } every { contactRepository.getEnrichedContacts() } returns flowOf(contacts) - every { receivedLocationDao.getLatestLocationsPerSender(any()) } returns flowOf(receivedLocations) + every { receivedLocationDao.getLatestLocationsPerSenderUnfiltered() } returns flowOf(receivedLocations) viewModel = MapViewModel(contactRepository, receivedLocationDao, locationSharingManager, announceDao) diff --git a/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelImageLoadingTest.kt b/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelImageLoadingTest.kt index 05846dd..e9257e3 100644 --- a/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelImageLoadingTest.kt +++ b/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelImageLoadingTest.kt @@ -12,6 +12,7 @@ import com.lxmf.messenger.repository.SettingsRepository import com.lxmf.messenger.reticulum.model.Identity import com.lxmf.messenger.reticulum.protocol.ServiceReticulumProtocol import com.lxmf.messenger.service.ActiveConversationManager +import com.lxmf.messenger.service.LocationSharingManager import com.lxmf.messenger.service.PropagationNodeManager import com.lxmf.messenger.ui.model.ImageCache import io.mockk.Runs @@ -70,6 +71,7 @@ class MessagingViewModelImageLoadingTest { private lateinit var activeConversationManager: ActiveConversationManager private lateinit var settingsRepository: SettingsRepository private lateinit var propagationNodeManager: PropagationNodeManager + private lateinit var locationSharingManager: LocationSharingManager private lateinit var viewModel: MessagingViewModel @Before @@ -86,6 +88,10 @@ class MessagingViewModelImageLoadingTest { activeConversationManager = mockk(relaxed = true) settingsRepository = mockk(relaxed = true) propagationNodeManager = mockk(relaxed = true) + locationSharingManager = mockk(relaxed = true) + + // Mock locationSharingManager flows + every { locationSharingManager.activeSessions } returns MutableStateFlow(emptyList()) // Setup default mock behaviors every { conversationRepository.getMessages(any()) } returns flowOf(emptyList()) @@ -110,6 +116,7 @@ class MessagingViewModelImageLoadingTest { activeConversationManager = activeConversationManager, settingsRepository = settingsRepository, propagationNodeManager = propagationNodeManager, + locationSharingManager = locationSharingManager, ) } diff --git a/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelTest.kt b/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelTest.kt index a28ed57..249e2a1 100644 --- a/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelTest.kt +++ b/app/src/test/java/com/lxmf/messenger/viewmodel/MessagingViewModelTest.kt @@ -12,6 +12,7 @@ import com.lxmf.messenger.reticulum.protocol.DeliveryStatusUpdate import com.lxmf.messenger.reticulum.protocol.MessageReceipt import com.lxmf.messenger.reticulum.protocol.ServiceReticulumProtocol import com.lxmf.messenger.service.ActiveConversationManager +import com.lxmf.messenger.service.LocationSharingManager import com.lxmf.messenger.service.PropagationNodeManager import io.mockk.* import kotlinx.coroutines.Dispatchers @@ -61,6 +62,7 @@ class MessagingViewModelTest { private lateinit var activeConversationManager: ActiveConversationManager private lateinit var settingsRepository: SettingsRepository private lateinit var propagationNodeManager: PropagationNodeManager + private lateinit var locationSharingManager: LocationSharingManager private val testPeerHash = "abcdef0123456789abcdef0123456789" // Valid 32-char hex hash private val testPeerName = "Test Peer" @@ -82,9 +84,14 @@ class MessagingViewModelTest { activeConversationManager = mockk(relaxed = true) settingsRepository = mockk(relaxed = true) propagationNodeManager = mockk(relaxed = true) + locationSharingManager = mockk(relaxed = true) + + // Mock locationSharingManager flows + every { locationSharingManager.activeSessions } returns MutableStateFlow(emptyList()) // Mock default contact repository behavior every { contactRepository.hasContactFlow(any()) } returns flowOf(false) + every { contactRepository.getEnrichedContacts() } returns flowOf(emptyList()) coEvery { contactRepository.hasContact(any()) } returns false coEvery { contactRepository.addContactFromConversation(any(), any()) } returns Result.success(Unit) coEvery { contactRepository.deleteContact(any()) } just Runs @@ -133,6 +140,7 @@ class MessagingViewModelTest { activeConversationManager, settingsRepository, propagationNodeManager, + locationSharingManager, ) @Test @@ -414,12 +422,15 @@ class MessagingViewModelTest { val failingContactRepository: ContactRepository = mockk() every { failingContactRepository.hasContactFlow(any()) } returns flowOf(false) + every { failingContactRepository.getEnrichedContacts() } returns flowOf(emptyList()) val failingActiveConversationManager: ActiveConversationManager = mockk(relaxed = true) val failingSettingsRepository: SettingsRepository = mockk(relaxed = true) val failingPropagationNodeManager: PropagationNodeManager = mockk(relaxed = true) + val failingLocationSharingManager: LocationSharingManager = mockk(relaxed = true) every { failingPropagationNodeManager.isSyncing } returns MutableStateFlow(false) every { failingPropagationNodeManager.manualSyncResult } returns MutableSharedFlow() + every { failingLocationSharingManager.activeSessions } returns MutableStateFlow(emptyList()) val viewModelWithoutIdentity = MessagingViewModel( @@ -430,6 +441,7 @@ class MessagingViewModelTest { failingActiveConversationManager, failingSettingsRepository, failingPropagationNodeManager, + failingLocationSharingManager, ) // Attempt to send message @@ -870,6 +882,7 @@ class MessagingViewModelTest { activeConversationManager, settingsRepository, propagationNodeManager, + locationSharingManager, ) advanceUntilIdle() @@ -929,6 +942,7 @@ class MessagingViewModelTest { activeConversationManager, settingsRepository, propagationNodeManager, + locationSharingManager, ) advanceUntilIdle() @@ -984,6 +998,7 @@ class MessagingViewModelTest { activeConversationManager, settingsRepository, propagationNodeManager, + locationSharingManager, ) advanceUntilIdle() @@ -1029,6 +1044,7 @@ class MessagingViewModelTest { activeConversationManager, settingsRepository, propagationNodeManager, + locationSharingManager, ) advanceUntilIdle() diff --git a/app/src/test/java/com/lxmf/messenger/viewmodel/SettingsViewModelTest.kt b/app/src/test/java/com/lxmf/messenger/viewmodel/SettingsViewModelTest.kt index 9396a0f..091a9fb 100644 --- a/app/src/test/java/com/lxmf/messenger/viewmodel/SettingsViewModelTest.kt +++ b/app/src/test/java/com/lxmf/messenger/viewmodel/SettingsViewModelTest.kt @@ -9,6 +9,7 @@ import com.lxmf.messenger.reticulum.model.NetworkStatus import com.lxmf.messenger.reticulum.protocol.ReticulumProtocol import com.lxmf.messenger.service.AvailableRelaysState import com.lxmf.messenger.service.InterfaceConfigManager +import com.lxmf.messenger.service.LocationSharingManager import com.lxmf.messenger.service.PropagationNodeManager import com.lxmf.messenger.service.RelayInfo import com.lxmf.messenger.ui.theme.PresetTheme @@ -55,6 +56,7 @@ class SettingsViewModelTest { private lateinit var reticulumProtocol: ReticulumProtocol private lateinit var interfaceConfigManager: InterfaceConfigManager private lateinit var propagationNodeManager: PropagationNodeManager + private lateinit var locationSharingManager: LocationSharingManager private lateinit var viewModel: SettingsViewModel // Mutable flows for controlling test scenarios @@ -84,6 +86,10 @@ class SettingsViewModelTest { reticulumProtocol = mockk(relaxed = true) interfaceConfigManager = mockk(relaxed = true) propagationNodeManager = mockk(relaxed = true) + locationSharingManager = mockk(relaxed = true) + + // Mock locationSharingManager flows + every { locationSharingManager.activeSessions } returns MutableStateFlow(emptyList()) // Setup repository flow mocks every { settingsRepository.preferOwnInstanceFlow } returns preferOwnInstanceFlow @@ -130,6 +136,7 @@ class SettingsViewModelTest { reticulumProtocol = reticulumProtocol, interfaceConfigManager = interfaceConfigManager, propagationNodeManager = propagationNodeManager, + locationSharingManager = locationSharingManager, ) } @@ -1423,6 +1430,7 @@ class SettingsViewModelTest { reticulumProtocol = serviceProtocol, interfaceConfigManager = interfaceConfigManager, propagationNodeManager = propagationNodeManager, + locationSharingManager = locationSharingManager, ) viewModel.state.test { @@ -1464,6 +1472,7 @@ class SettingsViewModelTest { reticulumProtocol = serviceProtocol, interfaceConfigManager = interfaceConfigManager, propagationNodeManager = propagationNodeManager, + locationSharingManager = locationSharingManager, ) viewModel.state.test { @@ -1955,6 +1964,7 @@ class SettingsViewModelTest { reticulumProtocol = serviceProtocol, interfaceConfigManager = interfaceConfigManager, propagationNodeManager = propagationNodeManager, + locationSharingManager = locationSharingManager, ) // Wait for any potential async operations to settle @@ -1993,6 +2003,7 @@ class SettingsViewModelTest { reticulumProtocol = serviceProtocol, interfaceConfigManager = interfaceConfigManager, propagationNodeManager = propagationNodeManager, + locationSharingManager = locationSharingManager, ) // The ViewModel should be created successfully with ServiceReticulumProtocol