mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat/decoupling (#4685)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
40244f8337
commit
2c49db8041
254 changed files with 5132 additions and 2666 deletions
|
|
@ -44,6 +44,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
|
|||
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
|
|||
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -662,7 +663,7 @@ class NordicBleInterfaceTest {
|
|||
advanceUntilIdle()
|
||||
|
||||
// Verify handleFromRadio was called directly with the payload
|
||||
verify(timeout = 2000) { service.handleFromRadio(p = payload) }
|
||||
verify(timeout = 2000) { service.handleFromRadio(payload) }
|
||||
|
||||
nordicInterface.close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.mockk.confirmVerified
|
|||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
class StreamInterfaceTest {
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.app.Notification
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import io.mockk.mockk
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
|
|
@ -64,15 +64,15 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
|
|||
|
||||
override fun showAlertNotification(contactKey: String, name: String, alert: String) {}
|
||||
|
||||
override fun showNewNodeSeenNotification(node: NodeEntity) {}
|
||||
override fun showNewNodeSeenNotification(node: Node) {}
|
||||
|
||||
override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {}
|
||||
override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {}
|
||||
|
||||
override fun showClientNotification(clientNotification: ClientNotification) {}
|
||||
|
||||
override fun cancelMessageNotification(contactKey: String) {}
|
||||
|
||||
override fun cancelLowBatteryNotification(node: NodeEntity) {}
|
||||
override fun cancelLowBatteryNotification(node: Node) {}
|
||||
|
||||
override fun clearClientNotification(notification: ClientNotification) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MyNodeInfo
|
||||
import org.meshtastic.proto.NodeInfo
|
||||
import org.meshtastic.proto.QueueStatus
|
||||
|
||||
class FromRadioPacketHandlerTest {
|
||||
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
|
||||
private val router: MeshRouter = mockk(relaxed = true)
|
||||
private val mqttManager: MeshMqttManager = mockk(relaxed = true)
|
||||
private val packetHandler: PacketHandler = mockk(relaxed = true)
|
||||
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
|
||||
|
||||
private lateinit var handler: FromRadioPacketHandler
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
handler = FromRadioPacketHandler(serviceRepository, router, mqttManager, packetHandler, serviceNotifications)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleFromRadio routes MY_INFO to configFlowManager`() {
|
||||
val myInfo = MyNodeInfo(my_node_num = 1234)
|
||||
val proto = FromRadio(my_info = myInfo)
|
||||
|
||||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { router.configFlowManager.handleMyInfo(myInfo) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleFromRadio routes METADATA to configFlowManager`() {
|
||||
val metadata = DeviceMetadata(firmware_version = "v1.0")
|
||||
val proto = FromRadio(metadata = metadata)
|
||||
|
||||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { router.configFlowManager.handleLocalMetadata(metadata) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleFromRadio routes NODE_INFO to configFlowManager and updates status`() {
|
||||
val nodeInfo = NodeInfo(num = 1234)
|
||||
val proto = FromRadio(node_info = nodeInfo)
|
||||
|
||||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { router.configFlowManager.handleNodeInfo(nodeInfo) }
|
||||
verify { serviceRepository.setConnectionProgress(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleFromRadio routes CONFIG_COMPLETE_ID to configFlowManager`() {
|
||||
val nonce = 69420
|
||||
val proto = FromRadio(config_complete_id = nonce)
|
||||
|
||||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { router.configFlowManager.handleConfigComplete(nonce) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleFromRadio routes QUEUESTATUS to packetHandler`() {
|
||||
val queueStatus = QueueStatus(free = 10)
|
||||
val proto = FromRadio(queueStatus = queueStatus)
|
||||
|
||||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { packetHandler.handleQueueStatus(queueStatus) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleFromRadio routes CONFIG to configHandler`() {
|
||||
val config = Config(lora = Config.LoRaConfig(use_preset = true))
|
||||
val proto = FromRadio(config = config)
|
||||
|
||||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { router.configHandler.handleDeviceConfig(config) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleFromRadio routes CLIENTNOTIFICATION to serviceRepository and notifications`() {
|
||||
val notification = ClientNotification(message = "test")
|
||||
val proto = FromRadio(clientNotification = notification)
|
||||
|
||||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { serviceRepository.setClientNotification(notification) }
|
||||
verify { serviceNotifications.showClientNotification(notification) }
|
||||
verify { packetHandler.removeResponse(0, complete = false) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
|
||||
class MeshCommandSenderHopLimitTest {
|
||||
|
||||
private val packetHandler: PacketHandler = mockk(relaxed = true)
|
||||
private val nodeManager = MeshNodeManager()
|
||||
private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true)
|
||||
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
|
||||
|
||||
private val localConfigFlow = MutableStateFlow(LocalConfig())
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val testScope = CoroutineScope(testDispatcher)
|
||||
|
||||
private lateinit var commandSender: MeshCommandSender
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val connectedFlow = MutableStateFlow(ConnectionState.Connected)
|
||||
every { connectionStateHolder.connectionState } returns connectedFlow
|
||||
every { radioConfigRepository.localConfigFlow } returns localConfigFlow
|
||||
|
||||
commandSender = MeshCommandSender(packetHandler, nodeManager, connectionStateHolder, radioConfigRepository)
|
||||
commandSender.start(testScope)
|
||||
nodeManager.myNodeNum = 123
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData uses default hop limit when config hop limit is zero`() = runTest(testDispatcher) {
|
||||
val packet =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
bytes = byteArrayOf(1, 2, 3).toByteString(),
|
||||
dataType = 1, // PortNum.TEXT_MESSAGE_APP
|
||||
)
|
||||
|
||||
val meshPacketSlot = slot<MeshPacket>()
|
||||
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
|
||||
|
||||
// Ensure localConfig has lora.hop_limit = 0
|
||||
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 0))
|
||||
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
|
||||
val capturedHopLimit = meshPacketSlot.captured.hop_limit ?: 0
|
||||
assertTrue("Hop limit should be greater than 0, but was $capturedHopLimit", capturedHopLimit > 0)
|
||||
assertEquals(3, capturedHopLimit)
|
||||
assertEquals(3, meshPacketSlot.captured.hop_start)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData respects non-zero hop limit from config`() = runTest(testDispatcher) {
|
||||
val packet =
|
||||
DataPacket(to = DataPacket.ID_BROADCAST, bytes = byteArrayOf(1, 2, 3).toByteString(), dataType = 1)
|
||||
|
||||
val meshPacketSlot = slot<MeshPacket>()
|
||||
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
|
||||
|
||||
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 7))
|
||||
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
assertEquals(7, meshPacketSlot.captured.hop_limit)
|
||||
assertEquals(7, meshPacketSlot.captured.hop_start)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `requestUserInfo sets hopStart equal to hopLimit`() = runTest(testDispatcher) {
|
||||
val destNum = 12345
|
||||
val meshPacketSlot = slot<MeshPacket>()
|
||||
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
|
||||
|
||||
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 6))
|
||||
|
||||
// Mock node manager interactions
|
||||
nodeManager.nodeDBbyNodeNum.remove(destNum)
|
||||
|
||||
commandSender.requestUserInfo(destNum)
|
||||
|
||||
verify { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
assertEquals("Hop Limit should be 6", 6, meshPacketSlot.captured.hop_limit)
|
||||
assertEquals("Hop Start should be 6", 6, meshPacketSlot.captured.hop_start)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
class MeshCommandSenderTest {
|
||||
|
||||
private lateinit var commandSender: MeshCommandSender
|
||||
private lateinit var nodeManager: MeshNodeManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeManager = MeshNodeManager()
|
||||
commandSender = MeshCommandSender(null, nodeManager, null, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generatePacketId produces unique non-zero IDs`() {
|
||||
val ids = mutableSetOf<Int>()
|
||||
repeat(1000) {
|
||||
val id = commandSender.generatePacketId()
|
||||
assertNotEquals(0, id)
|
||||
ids.add(id)
|
||||
}
|
||||
assertEquals(1000, ids.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolveNodeNum handles broadcast ID`() {
|
||||
assertEquals(DataPacket.NODENUM_BROADCAST, commandSender.resolveNodeNum(DataPacket.ID_BROADCAST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolveNodeNum handles hex ID with exclamation mark`() {
|
||||
assertEquals(123, commandSender.resolveNodeNum("!0000007b"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolveNodeNum handles custom node ID from database`() {
|
||||
val nodeNum = 456
|
||||
val userId = "custom_id"
|
||||
val entity = NodeEntity(num = nodeNum, user = User(id = userId))
|
||||
nodeManager.nodeDBbyNodeNum[nodeNum] = entity
|
||||
nodeManager.nodeDBbyID[userId] = entity
|
||||
|
||||
assertEquals(nodeNum, commandSender.resolveNodeNum(userId))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `resolveNodeNum throws for unknown ID`() {
|
||||
commandSender.resolveNodeNum("unknown")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import org.meshtastic.proto.LocalStats
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.ToRadio
|
||||
|
||||
class MeshConnectionManagerTest {
|
||||
|
||||
private val context: Context = mockk(relaxed = true)
|
||||
private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true)
|
||||
private val connectionStateHolder = ConnectionStateHandler()
|
||||
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
|
||||
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
|
||||
private val uiPrefs: UiPrefs = mockk(relaxed = true)
|
||||
private val packetHandler: PacketHandler = mockk(relaxed = true)
|
||||
private val nodeRepository: NodeRepository = mockk(relaxed = true)
|
||||
private val locationManager: MeshLocationManager = mockk(relaxed = true)
|
||||
private val mqttManager: MeshMqttManager = mockk(relaxed = true)
|
||||
private val historyManager: MeshHistoryManager = mockk(relaxed = true)
|
||||
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
|
||||
private val commandSender: MeshCommandSender = mockk(relaxed = true)
|
||||
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
|
||||
private val analytics: PlatformAnalytics = mockk(relaxed = true)
|
||||
private val packetRepository: PacketRepository = mockk(relaxed = true)
|
||||
private val workManager: WorkManager = mockk(relaxed = true)
|
||||
private val radioConnectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
private val localConfigFlow = MutableStateFlow(LocalConfig())
|
||||
private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig())
|
||||
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
|
||||
private lateinit var manager: MeshConnectionManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic("org.jetbrains.compose.resources.StringResourcesKt")
|
||||
mockkStatic("androidx.glance.appwidget.GlanceAppWidgetKt")
|
||||
coEvery { org.jetbrains.compose.resources.getString(any()) } returns "Mocked String"
|
||||
coEvery { org.jetbrains.compose.resources.getString(any(), *anyVararg()) } returns "Mocked String"
|
||||
coEvery { any<GlanceAppWidget>().updateAll(any()) } returns Unit
|
||||
|
||||
every { radioInterfaceService.connectionState } returns radioConnectionState
|
||||
every { radioConfigRepository.localConfigFlow } returns localConfigFlow
|
||||
every { radioConfigRepository.moduleConfigFlow } returns moduleConfigFlow
|
||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow<MyNodeEntity?>(null)
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow<Node?>(null)
|
||||
every { nodeRepository.localStats } returns MutableStateFlow(LocalStats())
|
||||
|
||||
manager =
|
||||
MeshConnectionManager(
|
||||
context,
|
||||
radioInterfaceService,
|
||||
connectionStateHolder,
|
||||
serviceBroadcasts,
|
||||
serviceNotifications,
|
||||
uiPrefs,
|
||||
packetHandler,
|
||||
nodeRepository,
|
||||
locationManager,
|
||||
mqttManager,
|
||||
historyManager,
|
||||
radioConfigRepository,
|
||||
commandSender,
|
||||
nodeManager,
|
||||
analytics,
|
||||
packetRepository,
|
||||
workManager,
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic("org.jetbrains.compose.resources.StringResourcesKt")
|
||||
unmockkStatic("androidx.glance.appwidget.GlanceAppWidgetKt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Connected state triggers broadcast and config start`() = runTest(testDispatcher) {
|
||||
manager.start(backgroundScope)
|
||||
radioConnectionState.value = ConnectionState.Connected
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
"State should be Connecting after radio Connected",
|
||||
ConnectionState.Connecting,
|
||||
connectionStateHolder.connectionState.value,
|
||||
)
|
||||
verify { serviceBroadcasts.broadcastConnection() }
|
||||
verify { packetHandler.sendToRadio(any<ToRadio>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Disconnected state stops services`() = runTest(testDispatcher) {
|
||||
manager.start(backgroundScope)
|
||||
// Transition to Connected first so that Disconnected actually does something
|
||||
radioConnectionState.value = ConnectionState.Connected
|
||||
advanceUntilIdle()
|
||||
|
||||
radioConnectionState.value = ConnectionState.Disconnected
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
"State should be Disconnected after radio Disconnected",
|
||||
ConnectionState.Disconnected,
|
||||
connectionStateHolder.connectionState.value,
|
||||
)
|
||||
verify { packetHandler.stopPacketQueue() }
|
||||
verify { locationManager.stop() }
|
||||
verify { mqttManager.stop() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DeviceSleep behavior when power saving is off maps to Disconnected`() = runTest(testDispatcher) {
|
||||
// Power saving disabled + Role CLIENT
|
||||
val config =
|
||||
LocalConfig(
|
||||
power = Config.PowerConfig(is_power_saving = false),
|
||||
device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT),
|
||||
)
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
|
||||
|
||||
manager.start(backgroundScope)
|
||||
advanceUntilIdle()
|
||||
|
||||
radioConnectionState.value = ConnectionState.DeviceSleep
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
"State should be Disconnected when power saving is off",
|
||||
ConnectionState.Disconnected,
|
||||
connectionStateHolder.connectionState.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DeviceSleep behavior when power saving is on stays in DeviceSleep`() = runTest(testDispatcher) {
|
||||
// Power saving enabled
|
||||
val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true))
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
|
||||
|
||||
manager.start(backgroundScope)
|
||||
advanceUntilIdle()
|
||||
|
||||
radioConnectionState.value = ConnectionState.DeviceSleep
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
"State should stay in DeviceSleep when power saving is on",
|
||||
ConnectionState.DeviceSleep,
|
||||
connectionStateHolder.connectionState.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onRadioConfigLoaded enqueues queued packets and sets time`() = runTest(testDispatcher) {
|
||||
manager.start(backgroundScope)
|
||||
val packetId = 456
|
||||
val dataPacket = mockk<DataPacket>(relaxed = true)
|
||||
every { dataPacket.id } returns packetId
|
||||
coEvery { packetRepository.getQueuedPackets() } returns listOf(dataPacket)
|
||||
|
||||
manager.onRadioConfigLoaded()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
workManager.enqueueUniqueWork(
|
||||
match<String> { it.startsWith(SendMessageWorker.WORK_NAME_PREFIX) },
|
||||
any<ExistingWorkPolicy>(),
|
||||
any<OneTimeWorkRequest>(),
|
||||
)
|
||||
}
|
||||
verify { commandSender.sendAdmin(any(), initFn = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onNodeDbReady starts MQTT and requests history`() = runTest(testDispatcher) {
|
||||
val moduleConfig = mockk<LocalModuleConfig>(relaxed = true)
|
||||
every { moduleConfig.mqtt } returns ModuleConfig.MQTTConfig(enabled = true)
|
||||
every { moduleConfig.store_forward } returns ModuleConfig.StoreForwardConfig(enabled = true)
|
||||
moduleConfigFlow.value = moduleConfig
|
||||
|
||||
manager.start(backgroundScope)
|
||||
manager.onNodeDbReady()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { mqttManager.start(any(), true, any()) }
|
||||
verify { historyManager.requestHistoryReplay("onNodeDbReady", any(), any(), "Unknown") }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import dagger.Lazy
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.service.filter.MessageFilterService
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.StoreForwardPlusPlus
|
||||
|
||||
class MeshDataHandlerTest {
|
||||
|
||||
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
|
||||
private val packetHandler: PacketHandler = mockk(relaxed = true)
|
||||
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
|
||||
private val packetRepository: PacketRepository = mockk(relaxed = true)
|
||||
private val packetRepositoryLazy: Lazy<PacketRepository> = mockk { every { get() } returns packetRepository }
|
||||
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
|
||||
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
|
||||
private val analytics: PlatformAnalytics = mockk(relaxed = true)
|
||||
private val dataMapper: MeshDataMapper = mockk(relaxed = true)
|
||||
private val configHandler: MeshConfigHandler = mockk(relaxed = true)
|
||||
private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true)
|
||||
private val commandSender: MeshCommandSender = mockk(relaxed = true)
|
||||
private val historyManager: MeshHistoryManager = mockk(relaxed = true)
|
||||
private val meshPrefs: MeshPrefs = mockk(relaxed = true)
|
||||
private val connectionManager: MeshConnectionManager = mockk(relaxed = true)
|
||||
private val tracerouteHandler: MeshTracerouteHandler = mockk(relaxed = true)
|
||||
private val neighborInfoHandler: MeshNeighborInfoHandler = mockk(relaxed = true)
|
||||
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
|
||||
private val messageFilterService: MessageFilterService = mockk(relaxed = true)
|
||||
|
||||
private lateinit var meshDataHandler: MeshDataHandler
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic(android.util.Log::class)
|
||||
every { android.util.Log.d(any(), any()) } returns 0
|
||||
every { android.util.Log.i(any(), any()) } returns 0
|
||||
every { android.util.Log.w(any(), any<String>()) } returns 0
|
||||
every { android.util.Log.e(any(), any()) } returns 0
|
||||
|
||||
meshDataHandler =
|
||||
MeshDataHandler(
|
||||
nodeManager,
|
||||
packetHandler,
|
||||
serviceRepository,
|
||||
packetRepositoryLazy,
|
||||
serviceBroadcasts,
|
||||
serviceNotifications,
|
||||
analytics,
|
||||
dataMapper,
|
||||
configHandler,
|
||||
configFlowManager,
|
||||
commandSender,
|
||||
historyManager,
|
||||
meshPrefs,
|
||||
connectionManager,
|
||||
tracerouteHandler,
|
||||
neighborInfoHandler,
|
||||
radioConfigRepository,
|
||||
messageFilterService,
|
||||
)
|
||||
// Use UnconfinedTestDispatcher for running coroutines synchronously in tests
|
||||
meshDataHandler.start(CoroutineScope(UnconfinedTestDispatcher()))
|
||||
|
||||
every { nodeManager.myNodeNum } returns 123
|
||||
every { nodeManager.getMyId() } returns "!0000007b"
|
||||
|
||||
// Default behavior for dataMapper to return a valid DataPacket when requested
|
||||
every { dataMapper.toDataPacket(any()) } answers
|
||||
{
|
||||
val packet = firstArg<MeshPacket>()
|
||||
DataPacket(
|
||||
to = "to",
|
||||
channel = 0,
|
||||
bytes = packet.decoded?.payload,
|
||||
dataType = packet.decoded?.portnum?.value ?: 0,
|
||||
id = packet.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedData with SFPP LINK_PROVIDE updates SFPP status`() = runTest {
|
||||
val sfppMessage =
|
||||
StoreForwardPlusPlus(
|
||||
sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE,
|
||||
encapsulated_id = 999,
|
||||
encapsulated_from = 456,
|
||||
encapsulated_to = 789,
|
||||
encapsulated_rxtime = 1000,
|
||||
message = "EncryptedPayload".toByteArray().toByteString(),
|
||||
message_hash = "Hash".toByteArray().toByteString(),
|
||||
)
|
||||
|
||||
val payload = StoreForwardPlusPlus.ADAPTER.encode(sfppMessage).toByteString()
|
||||
val meshPacket =
|
||||
MeshPacket(
|
||||
from = 456,
|
||||
to = 123,
|
||||
decoded = Data(portnum = PortNum.STORE_FORWARD_PLUSPLUS_APP, payload = payload),
|
||||
id = 1001,
|
||||
)
|
||||
|
||||
meshDataHandler.handleReceivedData(meshPacket, 123)
|
||||
|
||||
// SFPP_ROUTING because commit_hash is empty
|
||||
coVerify {
|
||||
packetRepository.updateSFPPStatus(
|
||||
packetId = 999,
|
||||
from = 456,
|
||||
to = 789,
|
||||
hash = any(),
|
||||
status = MessageStatus.SFPP_ROUTING,
|
||||
rxTime = 1000L,
|
||||
myNodeNum = 123,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
class MeshDataMapperTest {
|
||||
|
||||
private val nodeManager: MeshNodeManager = mockk()
|
||||
private lateinit var mapper: MeshDataMapper
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mapper = MeshDataMapper(nodeManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toNodeID resolves broadcast correctly`() {
|
||||
every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST
|
||||
assertEquals(DataPacket.ID_BROADCAST, mapper.toNodeID(DataPacket.NODENUM_BROADCAST))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toNodeID resolves known node correctly`() {
|
||||
val nodeNum = 1234
|
||||
val nodeId = "!1234abcd"
|
||||
every { nodeManager.toNodeID(nodeNum) } returns nodeId
|
||||
|
||||
assertEquals(nodeId, mapper.toNodeID(nodeNum))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toNodeID resolves unknown node to default ID`() {
|
||||
val nodeNum = 1234
|
||||
val nodeId = DataPacket.nodeNumToDefaultId(nodeNum)
|
||||
every { nodeManager.toNodeID(nodeNum) } returns nodeId
|
||||
|
||||
assertEquals(nodeId, mapper.toNodeID(nodeNum))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDataPacket returns null when no decoded data`() {
|
||||
val packet = MeshPacket()
|
||||
assertNull(mapper.toDataPacket(packet))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDataPacket maps basic fields correctly`() {
|
||||
val nodeNum = 1234
|
||||
val nodeId = "!1234abcd"
|
||||
every { nodeManager.toNodeID(nodeNum) } returns nodeId
|
||||
every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST
|
||||
|
||||
val proto =
|
||||
MeshPacket(
|
||||
id = 42,
|
||||
from = nodeNum,
|
||||
to = DataPacket.NODENUM_BROADCAST,
|
||||
rx_time = 1600000000,
|
||||
rx_snr = 5.5f,
|
||||
rx_rssi = -100,
|
||||
hop_limit = 3,
|
||||
hop_start = 3,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.TEXT_MESSAGE_APP,
|
||||
payload = "hello".encodeToByteArray().toByteString(),
|
||||
reply_id = 123,
|
||||
),
|
||||
)
|
||||
|
||||
val result = mapper.toDataPacket(proto)
|
||||
assertNotNull(result)
|
||||
assertEquals(42, result!!.id)
|
||||
assertEquals(nodeId, result.from)
|
||||
assertEquals(DataPacket.ID_BROADCAST, result.to)
|
||||
assertEquals(1600000000000L, result.time)
|
||||
assertEquals(5.5f, result.snr)
|
||||
assertEquals(-100, result.rssi)
|
||||
assertEquals(PortNum.TEXT_MESSAGE_APP.value, result.dataType)
|
||||
assertEquals("hello", result.bytes?.utf8())
|
||||
assertEquals(123, result.replyId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDataPacket maps PKC channel correctly for encrypted packets`() {
|
||||
val proto = MeshPacket(pki_encrypted = true, channel = 1, decoded = Data())
|
||||
|
||||
every { nodeManager.toNodeID(any()) } returns "any"
|
||||
|
||||
val result = mapper.toDataPacket(proto)
|
||||
assertEquals(DataPacket.PKC_CHANNEL_INDEX, result!!.channel)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
class MeshMessageProcessorTest {
|
||||
|
||||
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
|
||||
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
|
||||
private val meshLogRepository: MeshLogRepository = mockk(relaxed = true)
|
||||
private val router: MeshRouter = mockk(relaxed = true)
|
||||
private val fromRadioDispatcher: FromRadioPacketHandler = mockk(relaxed = true)
|
||||
private val meshLogRepositoryLazy = dagger.Lazy { meshLogRepository }
|
||||
private val dataHandler: MeshDataHandler = mockk(relaxed = true)
|
||||
|
||||
private val isNodeDbReady = MutableStateFlow(false)
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val testScope = TestScope(testDispatcher)
|
||||
|
||||
private lateinit var processor: MeshMessageProcessor
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { nodeManager.isNodeDbReady } returns isNodeDbReady
|
||||
every { router.dataHandler } returns dataHandler
|
||||
processor =
|
||||
MeshMessageProcessor(nodeManager, serviceRepository, meshLogRepositoryLazy, router, fromRadioDispatcher)
|
||||
processor.start(testScope)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `early packets are buffered and flushed when DB is ready`() = runTest(testDispatcher) {
|
||||
val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
|
||||
|
||||
// 1. Database is NOT ready
|
||||
isNodeDbReady.value = false
|
||||
testScheduler.runCurrent() // trigger start() onEach
|
||||
|
||||
processor.handleReceivedMeshPacket(packet, 999)
|
||||
|
||||
// Verify that handleReceivedData has NOT been called yet
|
||||
verify(exactly = 0) { dataHandler.handleReceivedData(any(), any(), any(), any()) }
|
||||
|
||||
// 2. Database becomes ready
|
||||
isNodeDbReady.value = true
|
||||
testScheduler.runCurrent() // trigger onEach(true)
|
||||
|
||||
// Verify that handleReceivedData is now called with the buffered packet
|
||||
verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 123 }, any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `packets are processed immediately if DB is already ready`() = runTest(testDispatcher) {
|
||||
val packet = MeshPacket(id = 456, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
|
||||
|
||||
isNodeDbReady.value = true
|
||||
testScheduler.runCurrent()
|
||||
|
||||
processor.handleReceivedMeshPacket(packet, 999)
|
||||
|
||||
verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 456 }, any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `packets from local node are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 1234
|
||||
val packet = MeshPacket(from = myNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
|
||||
|
||||
isNodeDbReady.value = true
|
||||
testScheduler.runCurrent()
|
||||
|
||||
processor.handleReceivedMeshPacket(packet, myNodeNum)
|
||||
testScheduler.runCurrent() // wait for log insert job
|
||||
|
||||
coVerify { meshLogRepository.insert(match { log -> log.fromNum == MeshLog.NODE_NUM_LOCAL }) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `packets from remote nodes are logged with their node number`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 1234
|
||||
val remoteNodeNum = 5678
|
||||
val packet = MeshPacket(from = remoteNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
|
||||
|
||||
isNodeDbReady.value = true
|
||||
testScheduler.runCurrent()
|
||||
|
||||
processor.handleReceivedMeshPacket(packet, myNodeNum)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
coVerify { meshLogRepository.insert(match { log -> log.fromNum == remoteNodeNum }) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
class MeshNodeManagerTest {
|
||||
|
||||
private val nodeRepository: NodeRepository = mockk(relaxed = true)
|
||||
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
|
||||
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
|
||||
|
||||
private lateinit var nodeManager: MeshNodeManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeManager = MeshNodeManager(nodeRepository, serviceBroadcasts, serviceNotifications)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getOrCreateNodeInfo creates default user for unknown node`() {
|
||||
val nodeNum = 1234
|
||||
val result = nodeManager.getOrCreateNodeInfo(nodeNum)
|
||||
|
||||
assertNotNull(result)
|
||||
assertEquals(nodeNum, result.num)
|
||||
assertTrue(result.user.long_name?.startsWith("Meshtastic") == true)
|
||||
assertEquals(DataPacket.nodeNumToDefaultId(nodeNum), result.user.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedUser preserves existing user if incoming is default`() {
|
||||
val nodeNum = 1234
|
||||
val existingUser =
|
||||
User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2)
|
||||
|
||||
// Setup existing node
|
||||
nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser }
|
||||
|
||||
val incomingDefaultUser =
|
||||
User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET)
|
||||
|
||||
nodeManager.handleReceivedUser(nodeNum, incomingDefaultUser)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertEquals("My Custom Name", result!!.user.long_name)
|
||||
assertEquals(HardwareModel.TLORA_V2, result.user.hw_model)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedUser updates user if incoming is higher detail`() {
|
||||
val nodeNum = 1234
|
||||
val existingUser =
|
||||
User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET)
|
||||
|
||||
nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser }
|
||||
|
||||
val incomingDetailedUser =
|
||||
User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1)
|
||||
|
||||
nodeManager.handleReceivedUser(nodeNum, incomingDetailedUser)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertEquals("Real User", result!!.user.long_name)
|
||||
assertEquals(HardwareModel.TLORA_V1, result.user.hw_model)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedPosition updates node position`() {
|
||||
val nodeNum = 1234
|
||||
val position = Position(latitude_i = 450000000, longitude_i = 900000000)
|
||||
|
||||
nodeManager.handleReceivedPosition(nodeNum, 9999, position)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertNotNull(result!!.position)
|
||||
assertEquals(45.0, result.latitude, 0.0001)
|
||||
assertEquals(90.0, result.longitude, 0.0001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear resets internal state`() {
|
||||
nodeManager.updateNodeInfo(1234) { it.longName = "Test" }
|
||||
nodeManager.clear()
|
||||
|
||||
assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty())
|
||||
assertTrue(nodeManager.nodeDBbyID.isEmpty())
|
||||
assertNull(nodeManager.myNodeNum)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.QueueStatus
|
||||
import org.meshtastic.proto.ToRadio
|
||||
|
||||
class PacketHandlerTest {
|
||||
|
||||
private val packetRepository: PacketRepository = mockk(relaxed = true)
|
||||
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
|
||||
private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true)
|
||||
private val meshLogRepository: MeshLogRepository = mockk(relaxed = true)
|
||||
private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true)
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val testScope = TestScope(testDispatcher)
|
||||
|
||||
private lateinit var handler: PacketHandler
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
handler =
|
||||
PacketHandler(
|
||||
dagger.Lazy { packetRepository },
|
||||
serviceBroadcasts,
|
||||
radioInterfaceService,
|
||||
dagger.Lazy { meshLogRepository },
|
||||
connectionStateHolder,
|
||||
)
|
||||
handler.start(testScope)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendToRadio with ToRadio sends immediately`() {
|
||||
val toRadio = ToRadio(packet = MeshPacket(id = 123))
|
||||
|
||||
handler.sendToRadio(toRadio)
|
||||
|
||||
verify { radioInterfaceService.sendToRadio(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendToRadio with MeshPacket queues and sends when connected`() = runTest(testDispatcher) {
|
||||
val packet = MeshPacket(id = 456)
|
||||
every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||
|
||||
handler.sendToRadio(packet)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
verify { radioInterfaceService.sendToRadio(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleQueueStatus completes deferred`() = runTest(testDispatcher) {
|
||||
val packet = MeshPacket(id = 789)
|
||||
every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||
|
||||
handler.sendToRadio(packet)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val status =
|
||||
QueueStatus(
|
||||
mesh_packet_id = 789,
|
||||
res = 0, // Success
|
||||
free = 1,
|
||||
)
|
||||
|
||||
handler.handleQueueStatus(status)
|
||||
testScheduler.runCurrent()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `outgoing packets are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
|
||||
val toRadio = ToRadio(packet = packet)
|
||||
|
||||
handler.sendToRadio(toRadio)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
coVerify { meshLogRepository.insert(match { log -> log.fromNum == MeshLog.NODE_NUM_LOCAL }) }
|
||||
}
|
||||
}
|
||||
|
|
@ -19,35 +19,36 @@ package com.geeksville.mesh.service
|
|||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class MeshServiceBroadcastsTest {
|
||||
class ServiceBroadcastsTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private val connectionStateHolder = ConnectionStateHandler()
|
||||
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
|
||||
private lateinit var broadcasts: MeshServiceBroadcasts
|
||||
private lateinit var broadcasts: ServiceBroadcasts
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
broadcasts = MeshServiceBroadcasts(context, connectionStateHolder, serviceRepository)
|
||||
broadcasts = ServiceBroadcasts(context, serviceRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `broadcastConnection sends uppercase state string for ATAK`() {
|
||||
connectionStateHolder.setState(ConnectionState.Connected)
|
||||
every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||
|
||||
broadcasts.broadcastConnection()
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ class MeshServiceBroadcastsTest {
|
|||
|
||||
@Test
|
||||
fun `broadcastConnection sends legacy connection intent`() {
|
||||
connectionStateHolder.setState(ConnectionState.Connected)
|
||||
every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||
|
||||
broadcasts.broadcastConnection()
|
||||
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.meshtastic.proto.StoreAndForward
|
||||
|
||||
class StoreForwardHistoryRequestTest {
|
||||
|
||||
@Test
|
||||
fun `buildStoreForwardHistoryRequest copies positive parameters`() {
|
||||
val request =
|
||||
MeshHistoryManager.buildStoreForwardHistoryRequest(
|
||||
lastRequest = 42,
|
||||
historyReturnWindow = 15,
|
||||
historyReturnMax = 25,
|
||||
)
|
||||
|
||||
assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
|
||||
assertEquals(42, request.history?.last_request)
|
||||
assertEquals(15, request.history?.window)
|
||||
assertEquals(25, request.history?.history_messages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildStoreForwardHistoryRequest clamps non-positive parameters`() {
|
||||
val request =
|
||||
MeshHistoryManager.buildStoreForwardHistoryRequest(
|
||||
lastRequest = 0,
|
||||
historyReturnWindow = -1,
|
||||
historyReturnMax = 0,
|
||||
)
|
||||
|
||||
assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
|
||||
assertEquals(0, request.history?.last_request)
|
||||
assertEquals(0, request.history?.window)
|
||||
assertEquals(0, request.history?.history_messages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolveHistoryRequestParameters uses config values when positive`() {
|
||||
val (window, max) = MeshHistoryManager.resolveHistoryRequestParameters(window = 30, max = 10)
|
||||
|
||||
assertEquals(30, window)
|
||||
assertEquals(10, max)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolveHistoryRequestParameters falls back to defaults when non-positive`() {
|
||||
val (window, max) = MeshHistoryManager.resolveHistoryRequestParameters(window = 0, max = -5)
|
||||
|
||||
assertEquals(1440, window)
|
||||
assertEquals(100, max)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue