mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Integrate Mokkery and Turbine into KMP testing framework (#4845)
This commit is contained in:
parent
df3a094430
commit
dcbbc0823b
159 changed files with 1860 additions and 2809 deletions
|
|
@ -16,12 +16,9 @@
|
|||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class CapabilitiesTest {
|
||||
/*
|
||||
|
||||
|
||||
private fun caps(version: String?) = Capabilities(version, forceEnableAll = false)
|
||||
|
||||
|
|
@ -134,4 +131,6 @@ class CapabilitiesTest {
|
|||
assertTrue(DeviceVersion("2.7.12") == DeviceVersion("2.7.12"))
|
||||
assertFalse(DeviceVersion("2.6.9") >= DeviceVersion("2.7.0"))
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,9 @@
|
|||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
class ChannelOptionTest {
|
||||
/*
|
||||
|
||||
|
||||
/**
|
||||
* This test ensures that every `ModemPreset` defined in the protobufs has a corresponding entry in our
|
||||
|
|
@ -75,4 +72,6 @@ class ChannelOptionTest {
|
|||
ChannelOption.entries.size,
|
||||
)
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,142 +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 org.meshtastic.core.model
|
||||
|
||||
import android.os.Parcel
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class DataPacketParcelTest {
|
||||
|
||||
@Test
|
||||
fun `DataPacket parcelization round trip via writeToParcel and readParcelable`() {
|
||||
val original = createFullDataPacket()
|
||||
|
||||
val parcel = Parcel.obtain()
|
||||
// Use writeParcelable to include class information/nullability flag needed by readParcelable
|
||||
parcel.writeParcelable(original, 0)
|
||||
parcel.setDataPosition(0)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val created = parcel.readParcelable<DataPacket>(DataPacket::class.java.classLoader)
|
||||
parcel.recycle()
|
||||
|
||||
assertNotNull(created)
|
||||
assertDataPacketsEqual(original, created!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DataPacket manual readFromParcel matches writeToParcel`() {
|
||||
val original = createFullDataPacket()
|
||||
|
||||
// Write using generated writeToParcel (writes content only)
|
||||
val parcel = Parcel.obtain()
|
||||
original.writeToParcel(parcel, 0)
|
||||
parcel.setDataPosition(0)
|
||||
|
||||
// Read using manual readFromParcel
|
||||
// We start with an empty packet and populate it
|
||||
val restored = DataPacket(to = "dummy", channel = 0, text = "dummy")
|
||||
// Reset fields to ensure they are overwritten
|
||||
restored.to = null
|
||||
restored.from = null
|
||||
restored.bytes = null
|
||||
restored.sfppHash = null
|
||||
|
||||
restored.readFromParcel(parcel)
|
||||
parcel.recycle()
|
||||
|
||||
assertDataPacketsEqual(original, restored)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DataPacket with nulls handles parcelization correctly`() {
|
||||
val original =
|
||||
DataPacket(
|
||||
to = null,
|
||||
bytes = null,
|
||||
dataType = 99,
|
||||
from = null,
|
||||
time = 123L,
|
||||
status = null,
|
||||
replyId = null,
|
||||
relayNode = null,
|
||||
sfppHash = null,
|
||||
)
|
||||
|
||||
val parcel = Parcel.obtain()
|
||||
original.writeToParcel(parcel, 0)
|
||||
parcel.setDataPosition(0)
|
||||
|
||||
val restored = DataPacket(to = "dummy", channel = 0, text = "dummy")
|
||||
restored.readFromParcel(parcel)
|
||||
parcel.recycle()
|
||||
|
||||
assertDataPacketsEqual(original, restored)
|
||||
}
|
||||
|
||||
private fun createFullDataPacket(): DataPacket = DataPacket(
|
||||
to = "destNode",
|
||||
bytes = "Hello World".toByteArray().toByteString(),
|
||||
dataType = 1,
|
||||
from = "srcNode",
|
||||
time = 1234567890L,
|
||||
id = 42,
|
||||
status = MessageStatus.DELIVERED,
|
||||
hopLimit = 3,
|
||||
channel = 5,
|
||||
wantAck = true,
|
||||
hopStart = 7,
|
||||
snr = 12.5f,
|
||||
rssi = -80,
|
||||
replyId = 101,
|
||||
relayNode = 202,
|
||||
relays = 1,
|
||||
viaMqtt = true,
|
||||
emoji = 0x1F600,
|
||||
sfppHash = "sfpp".toByteArray().toByteString(),
|
||||
)
|
||||
|
||||
private fun assertDataPacketsEqual(expected: DataPacket, actual: DataPacket) {
|
||||
assertEquals(expected.to, actual.to)
|
||||
assertEquals(expected.bytes, actual.bytes)
|
||||
assertEquals(expected.dataType, actual.dataType)
|
||||
assertEquals(expected.from, actual.from)
|
||||
assertEquals(expected.time, actual.time)
|
||||
assertEquals(expected.id, actual.id)
|
||||
assertEquals(expected.status, actual.status)
|
||||
assertEquals(expected.hopLimit, actual.hopLimit)
|
||||
assertEquals(expected.channel, actual.channel)
|
||||
assertEquals(expected.wantAck, actual.wantAck)
|
||||
assertEquals(expected.hopStart, actual.hopStart)
|
||||
assertEquals(expected.snr, actual.snr, 0.001f)
|
||||
assertEquals(expected.rssi, actual.rssi)
|
||||
assertEquals(expected.replyId, actual.replyId)
|
||||
assertEquals(expected.relayNode, actual.relayNode)
|
||||
assertEquals(expected.relays, actual.relays)
|
||||
assertEquals(expected.viaMqtt, actual.viaMqtt)
|
||||
assertEquals(expected.emoji, actual.emoji)
|
||||
assertEquals(expected.sfppHash, actual.sfppHash)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +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 org.meshtastic.core.model
|
||||
|
||||
import android.os.Parcel
|
||||
import kotlinx.serialization.json.Json
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class DataPacketTest {
|
||||
@Test
|
||||
fun `DataPacket sfppHash is nullable and correctly set`() {
|
||||
val hash = byteArrayOf(1, 2, 3, 4).toByteString()
|
||||
val packet = DataPacket(to = "to", channel = 0, text = "hello").copy(sfppHash = hash)
|
||||
assertEquals(hash, packet.sfppHash)
|
||||
|
||||
val packetNoHash = DataPacket(to = "to", channel = 0, text = "hello")
|
||||
assertNull(packetNoHash.sfppHash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MessageStatus SFPP_CONFIRMED exists`() {
|
||||
val status = MessageStatus.SFPP_CONFIRMED
|
||||
assertEquals("SFPP_CONFIRMED", status.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DataPacket serialization preserves sfppHash`() {
|
||||
val hash = byteArrayOf(5, 6, 7, 8).toByteString()
|
||||
val packet =
|
||||
DataPacket(to = "to", channel = 0, text = "test")
|
||||
.copy(sfppHash = hash, status = MessageStatus.SFPP_CONFIRMED)
|
||||
|
||||
val json = Json { isLenient = true }
|
||||
val encoded = json.encodeToString(DataPacket.serializer(), packet)
|
||||
val decoded = json.decodeFromString(DataPacket.serializer(), encoded)
|
||||
|
||||
assertEquals(packet.status, decoded.status)
|
||||
assertEquals(hash, decoded.sfppHash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DataPacket equals and hashCode include sfppHash`() {
|
||||
val hash1 = byteArrayOf(1, 2, 3).toByteString()
|
||||
val hash2 = byteArrayOf(4, 5, 6).toByteString()
|
||||
val fixedTime = 1000L
|
||||
val base = DataPacket(to = "to", channel = 0, text = "text").copy(time = fixedTime)
|
||||
val p1 = base.copy(sfppHash = hash1)
|
||||
val p2 = base.copy(sfppHash = byteArrayOf(1, 2, 3).toByteString()) // same content
|
||||
val p3 = base.copy(sfppHash = hash2)
|
||||
val p4 = base.copy(sfppHash = null)
|
||||
|
||||
assertEquals(p1, p2)
|
||||
assertEquals(p1.hashCode(), p2.hashCode())
|
||||
|
||||
assertNotEquals(p1, p3)
|
||||
assertNotEquals(p1, p4)
|
||||
assertNotEquals(p1.hashCode(), p3.hashCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readFromParcel maintains alignment and updates all fields including bytes and dataType`() {
|
||||
val bytes = byteArrayOf(1, 2, 3).toByteString()
|
||||
val sfppHash = byteArrayOf(4, 5, 6).toByteString()
|
||||
val original =
|
||||
DataPacket(
|
||||
to = "recipient",
|
||||
bytes = bytes,
|
||||
dataType = 42,
|
||||
from = "sender",
|
||||
time = 123456789L,
|
||||
id = 100,
|
||||
status = MessageStatus.RECEIVED,
|
||||
hopLimit = 3,
|
||||
channel = 1,
|
||||
wantAck = true,
|
||||
hopStart = 5,
|
||||
snr = 1.5f,
|
||||
rssi = -90,
|
||||
replyId = 50,
|
||||
relayNode = 123,
|
||||
relays = 2,
|
||||
viaMqtt = true,
|
||||
emoji = 10,
|
||||
sfppHash = sfppHash,
|
||||
)
|
||||
|
||||
val parcel = Parcel.obtain()
|
||||
original.writeToParcel(parcel, 0)
|
||||
parcel.setDataPosition(0)
|
||||
|
||||
val packetToUpdate = DataPacket(to = "old", channel = 0, text = "old")
|
||||
packetToUpdate.readFromParcel(parcel)
|
||||
|
||||
// Verify that all fields were updated correctly
|
||||
assertEquals("recipient", packetToUpdate.to)
|
||||
assertEquals(bytes, packetToUpdate.bytes)
|
||||
assertEquals(42, packetToUpdate.dataType)
|
||||
assertEquals("sender", packetToUpdate.from)
|
||||
assertEquals(123456789L, packetToUpdate.time)
|
||||
assertEquals(100, packetToUpdate.id)
|
||||
assertEquals(MessageStatus.RECEIVED, packetToUpdate.status)
|
||||
assertEquals(3, packetToUpdate.hopLimit)
|
||||
assertEquals(1, packetToUpdate.channel)
|
||||
assertEquals(true, packetToUpdate.wantAck)
|
||||
assertEquals(5, packetToUpdate.hopStart)
|
||||
assertEquals(1.5f, packetToUpdate.snr)
|
||||
assertEquals(-90, packetToUpdate.rssi)
|
||||
assertEquals(50, packetToUpdate.replyId)
|
||||
assertEquals(123, packetToUpdate.relayNode)
|
||||
assertEquals(2, packetToUpdate.relays)
|
||||
assertEquals(true, packetToUpdate.viaMqtt)
|
||||
assertEquals(10, packetToUpdate.emoji)
|
||||
assertEquals(sfppHash, packetToUpdate.sfppHash)
|
||||
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
|
@ -16,10 +16,9 @@
|
|||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class DeviceVersionTest {
|
||||
/*
|
||||
|
||||
/** make sure we match the python and device code behavior */
|
||||
@Test
|
||||
fun canParse() {
|
||||
|
|
@ -28,4 +27,6 @@ class DeviceVersionTest {
|
|||
assertEquals(12357, DeviceVersion("1.23.57").asInt)
|
||||
assertEquals(12357, DeviceVersion("1.23.57.abde123").asInt)
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,9 @@
|
|||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import java.util.Locale
|
||||
|
||||
class NodeInfoTest {
|
||||
/*
|
||||
|
||||
private val model = HardwareModel.ANDROID_SIM
|
||||
private val node =
|
||||
listOf(
|
||||
|
|
@ -62,4 +55,6 @@ class NodeInfoTest {
|
|||
assertEquals("1.1 mi", node[1].distanceStr(node[4], Config.DisplayConfig.DisplayUnits.IMPERIAL.value))
|
||||
assertEquals("364 ft", node[1].distanceStr(node[3], Config.DisplayConfig.DisplayUnits.IMPERIAL.value))
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,9 @@
|
|||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PositionTest {
|
||||
/*
|
||||
|
||||
@Test
|
||||
fun degGood() {
|
||||
assertEquals(Position.degI(89.0), 890000000)
|
||||
|
|
@ -35,4 +33,6 @@ class PositionTest {
|
|||
val position = Position(37.1, 121.1, 35)
|
||||
assertTrue(position.time != 0)
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,95 +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 org.meshtastic.core.model.util
|
||||
|
||||
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 nodeIdLookup: NodeIdLookup = mockk()
|
||||
private lateinit var mapper: MeshDataMapper
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mapper = MeshDataMapper(nodeIdLookup)
|
||||
}
|
||||
|
||||
@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 { nodeIdLookup.toNodeID(nodeNum) } returns nodeId
|
||||
every { nodeIdLookup.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 { nodeIdLookup.toNodeID(any()) } returns "any"
|
||||
|
||||
val result = mapper.toDataPacket(proto)
|
||||
assertEquals(DataPacket.PKC_CHANNEL_INDEX, result!!.channel)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +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 org.meshtastic.core.model.util
|
||||
|
||||
import android.net.Uri
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import org.meshtastic.proto.User
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class SharedContactTest {
|
||||
|
||||
@Test
|
||||
fun testSharedContactUrlRoundTrip() {
|
||||
val original = SharedContact(user = User(long_name = "Suzume", short_name = "SZ"), node_num = 12345)
|
||||
val url = original.getSharedContactUrl()
|
||||
val parsed = url.toSharedContact()
|
||||
|
||||
assertEquals(original.node_num, parsed.node_num)
|
||||
assertEquals(original.user?.long_name, parsed.user?.long_name)
|
||||
assertEquals(original.user?.short_name, parsed.user?.short_name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWwwHostIsAccepted() {
|
||||
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
|
||||
val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "www.meshtastic.org")
|
||||
val url = Uri.parse(urlStr)
|
||||
val contact = url.toSharedContact()
|
||||
assertEquals("Suzume", contact.user?.long_name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongPathIsAccepted() {
|
||||
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
|
||||
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/contact/v/")
|
||||
val url = Uri.parse(urlStr)
|
||||
val contact = url.toSharedContact()
|
||||
assertEquals("Suzume", contact.user?.long_name)
|
||||
}
|
||||
|
||||
@Test(expected = MalformedMeshtasticUrlException::class)
|
||||
fun testInvalidHostThrows() {
|
||||
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
|
||||
val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "example.com")
|
||||
val url = Uri.parse(urlStr)
|
||||
url.toSharedContact()
|
||||
}
|
||||
|
||||
@Test(expected = MalformedMeshtasticUrlException::class)
|
||||
fun testInvalidPathThrows() {
|
||||
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
|
||||
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/wrong/")
|
||||
val url = Uri.parse(urlStr)
|
||||
url.toSharedContact()
|
||||
}
|
||||
|
||||
@Test(expected = MalformedMeshtasticUrlException::class)
|
||||
fun testMissingFragmentThrows() {
|
||||
val urlStr = "https://meshtastic.org/v/"
|
||||
val url = Uri.parse(urlStr)
|
||||
url.toSharedContact()
|
||||
}
|
||||
|
||||
@Test(expected = MalformedMeshtasticUrlException::class)
|
||||
fun testInvalidBase64Throws() {
|
||||
val urlStr = "https://meshtastic.org/v/#InvalidBase64!!!!"
|
||||
val url = Uri.parse(urlStr)
|
||||
url.toSharedContact()
|
||||
}
|
||||
|
||||
@Test(expected = MalformedMeshtasticUrlException::class)
|
||||
fun testInvalidProtoThrows() {
|
||||
// Tag 0 is invalid in Protobuf
|
||||
// 0x00 -> Tag 0, Type 0.
|
||||
// Base64 for 0x00 is "AA=="
|
||||
val urlStr = "https://meshtastic.org/v/#AA=="
|
||||
val url = Uri.parse(urlStr)
|
||||
url.toSharedContact()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +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 org.meshtastic.core.model.util
|
||||
|
||||
import android.net.Uri
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import org.meshtastic.proto.User
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class UriUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri handles channel share uri`() {
|
||||
val uri = Uri.parse("https://meshtastic.org/e/somechannel").toCommonUri()
|
||||
var channelCalled = false
|
||||
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
|
||||
assertTrue("Should handle channel URI", handled)
|
||||
assertTrue("Should invoke onChannel callback", channelCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri handles contact share uri`() {
|
||||
val uri = Uri.parse("https://meshtastic.org/v/somecontact").toCommonUri()
|
||||
var contactCalled = false
|
||||
val handled = handleMeshtasticUri(uri, onContact = { contactCalled = true })
|
||||
assertTrue("Should handle contact URI", handled)
|
||||
assertTrue("Should invoke onContact callback", contactCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri ignores other hosts`() {
|
||||
val uri = Uri.parse("https://example.com/e/somechannel").toCommonUri()
|
||||
val handled = handleMeshtasticUri(uri)
|
||||
assertFalse("Should not handle other hosts", handled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri ignores other paths`() {
|
||||
val uri = Uri.parse("https://meshtastic.org/other/path").toCommonUri()
|
||||
val handled = handleMeshtasticUri(uri)
|
||||
assertFalse("Should not handle unknown paths", handled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri handles case insensitivity`() {
|
||||
val uri = Uri.parse("https://MESHTASTIC.ORG/E/somechannel").toCommonUri()
|
||||
var channelCalled = false
|
||||
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
|
||||
assertTrue("Should handle mixed case URI", handled)
|
||||
assertTrue("Should invoke onChannel callback", channelCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri handles www host`() {
|
||||
val uri = Uri.parse("https://www.meshtastic.org/e/somechannel").toCommonUri()
|
||||
var channelCalled = false
|
||||
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
|
||||
assertTrue("Should handle www host", handled)
|
||||
assertTrue("Should invoke onChannel callback", channelCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri handles long channel path`() {
|
||||
val uri = Uri.parse("https://meshtastic.org/channel/e/somechannel").toCommonUri()
|
||||
var channelCalled = false
|
||||
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
|
||||
assertTrue("Should handle long channel path", handled)
|
||||
assertTrue("Should invoke onChannel callback", channelCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMeshtasticUri handles long contact path`() {
|
||||
val uri = Uri.parse("https://meshtastic.org/contact/v/somecontact").toCommonUri()
|
||||
var contactCalled = false
|
||||
val handled = handleMeshtasticUri(uri, onContact = { contactCalled = true })
|
||||
assertTrue("Should handle long contact path", handled)
|
||||
assertTrue("Should invoke onContact callback", contactCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dispatchMeshtasticUri dispatches correctly`() {
|
||||
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
|
||||
val uri = original.getSharedContactUrl()
|
||||
var contactReceived: SharedContact? = null
|
||||
|
||||
uri.dispatchMeshtasticUri(onChannel = {}, onContact = { contactReceived = it }, onInvalid = {})
|
||||
|
||||
assertTrue("Contact should be received", contactReceived != null)
|
||||
assertTrue("Name should match", contactReceived?.user?.long_name == "Suzume")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dispatchMeshtasticUri handles invalid variants via fallback`() {
|
||||
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
|
||||
// Manual override to an "unknown" path that handleMeshtasticUri would reject
|
||||
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/fallback/")
|
||||
val uri = Uri.parse(urlStr)
|
||||
|
||||
var contactReceived: SharedContact? = null
|
||||
|
||||
uri.dispatchMeshtasticUri(onChannel = {}, onContact = { contactReceived = it }, onInvalid = {})
|
||||
|
||||
// This should fail both handleMeshtasticUri AND toSharedContact because of path validation
|
||||
// So contactReceived should be null and onInvalid called (if provided)
|
||||
assertTrue("Contact should NOT be received with invalid path", contactReceived == null)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,10 +27,10 @@ import org.meshtastic.proto.MeshPacket
|
|||
*
|
||||
* This class is platform-agnostic and can be used in shared logic.
|
||||
*/
|
||||
class MeshDataMapper(private val nodeIdLookup: NodeIdLookup) {
|
||||
open class MeshDataMapper(private val nodeIdLookup: NodeIdLookup) {
|
||||
|
||||
/** Maps a [MeshPacket] to a [DataPacket], or returns null if the packet has no decoded data. */
|
||||
fun toDataPacket(packet: MeshPacket): DataPacket? {
|
||||
open fun toDataPacket(packet: MeshPacket): DataPacket? {
|
||||
val decoded = packet.decoded ?: return null
|
||||
return DataPacket(
|
||||
from = nodeIdLookup.toNodeID(packet.from),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue