From d1f91bac9858c8c99d6df4215e116b1bdefe22a6 Mon Sep 17 00:00:00 2001 From: Jai Patel <22jaipp@gmail.com> Date: Wed, 12 Nov 2025 14:41:35 -0700 Subject: [PATCH 1/9] Created Unit testing file specifically to test WifiConnection.kt --- .../components/WifiConnectionTest.kt | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 app/src/test/java/com/greybox/projectmesh/components/WifiConnectionTest.kt diff --git a/app/src/test/java/com/greybox/projectmesh/components/WifiConnectionTest.kt b/app/src/test/java/com/greybox/projectmesh/components/WifiConnectionTest.kt new file mode 100644 index 000000000..12687ec29 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/components/WifiConnectionTest.kt @@ -0,0 +1,127 @@ +package com.greybox.projectmesh.components + +import org.junit.Assert.* +import org.junit.Test +import java.lang.reflect.Field +import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig + +/** + * JVM-only tests for simple data + status in WifiConnection.kt. + * No Android/Compose/Mockito required. + * + * NOTE: We allocate a dummy WifiConnectConfig via Unsafe and NEVER call its methods. + * We also avoid ConnectRequest.equals()/hashCode() because that would call + * WifiConnectConfig.hashCode() internally (which can NPE if fields are null). + */ +class WifiConnectionTest { + + // ----------------------------- + // Enum: stages must exist + // ----------------------------- + @Test + fun checkAllStatusesExist() { + val all = ConnectWifiLauncherStatus.values().toSet() + assertTrue(ConnectWifiLauncherStatus.INACTIVE in all) + assertTrue(ConnectWifiLauncherStatus.REQUESTING_PERMISSION in all) + assertTrue(ConnectWifiLauncherStatus.LOOKING_FOR_NETWORK in all) + assertTrue(ConnectWifiLauncherStatus.REQUESTING_LINK in all) + assertEquals(4, all.size) + } + + // ----------------------------------------------- + // Result model: failure shape must look correct + // ----------------------------------------------- + @Test + fun checkFailureResultLooksRight() { + val error = Exception("expected failure") + val result = ConnectWifiLauncherResult( + hotspotConfig = null, + exception = error, + isWifiConnected = false + ) + assertFalse(result.isWifiConnected) + assertNull(result.hotspotConfig) + assertNotNull(result.exception) + assertEquals("expected failure", result.exception?.message) + } + + // ------------------------------------------------------- + // Result model: data-class copy/equals/hashCode sanity + // (safe because hotspotConfig = null) + // ------------------------------------------------------- + @Test + fun checkResultCopiesAndComparesCorrectly() { + val first = ConnectWifiLauncherResult( + hotspotConfig = null, + exception = Exception("boom"), + isWifiConnected = false + ) + val same = first.copy() + val different = first.copy(exception = Exception("other")) + + assertEquals(first, same) + assertEquals(first.hashCode(), same.hashCode()) + assertNotEquals(first, different) + } + + // ========================================= + // ConnectRequest: JVM tests (no Mockito) + // Avoid equals()/hashCode() on the whole object. + // ========================================= + + @Test + fun connectRequest_defaultTimeIsZero() { + val cfg = unsafeInstance() + val req = ConnectRequest(connectConfig = cfg) + assertEquals(0L, req.receivedTime) + assertSame(cfg, req.connectConfig) // same reference + } + + @Test + fun connectRequest_customTimePreserved() { + val cfg = unsafeInstance() + val req = ConnectRequest(receivedTime = 123456789L, connectConfig = cfg) + assertEquals(123456789L, req.receivedTime) + assertSame(cfg, req.connectConfig) + } + + @Test + fun connectRequest_copyPreservesConfigAndChangesTime() { + val cfg = unsafeInstance() + val original = ConnectRequest(receivedTime = 100L, connectConfig = cfg) + val copiedSame = original.copy() + val copiedChanged = original.copy(receivedTime = 200L) + + // field-wise checks (no equals()/hashCode()) + assertEquals(100L, copiedSame.receivedTime) + assertSame(cfg, copiedSame.connectConfig) + + assertEquals(200L, copiedChanged.receivedTime) + assertSame(cfg, copiedChanged.connectConfig) + + // sanity: copies are distinct instances + assertNotSame(original, copiedSame) + assertNotSame(original, copiedChanged) + } + + // ------------------------------------------------------- + // Tiny Unsafe helper for constructor-less allocation + // ------------------------------------------------------- + @Suppress("UNCHECKED_CAST") + private inline fun unsafeInstance(): T { + val unsafe = getUnsafe() + val allocate = unsafe.javaClass.getMethod("allocateInstance", Class::class.java) + return allocate.invoke(unsafe, T::class.java) as T + } + + private fun getUnsafe(): Any { + val clazz = try { + Class.forName("sun.misc.Unsafe") + } catch (_: ClassNotFoundException) { + Class.forName("jdk.internal.misc.Unsafe") + } + val f: Field = clazz.getDeclaredField("theUnsafe") + f.isAccessible = true + return f.get(null) + } +} From e624eff582c9a23b02c1729b8067f5c12b0ca489 Mon Sep 17 00:00:00 2001 From: Jai Patel <22jaipp@gmail.com> Date: Wed, 12 Nov 2025 15:36:27 -0700 Subject: [PATCH 2/9] Created Unit testing file specifically to test WifiConnection.kt --- .../projectmesh/db/MeshDatabaseTest.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/test/java/com/greybox/projectmesh/db/MeshDatabaseTest.kt diff --git a/app/src/test/java/com/greybox/projectmesh/db/MeshDatabaseTest.kt b/app/src/test/java/com/greybox/projectmesh/db/MeshDatabaseTest.kt new file mode 100644 index 000000000..e8d812006 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/db/MeshDatabaseTest.kt @@ -0,0 +1,54 @@ +package com.greybox.projectmesh.db + +import org.junit.Assert.* +import org.junit.Test +import java.lang.reflect.Modifier +import com.greybox.projectmesh.messaging.data.dao.MessageDao +import com.greybox.projectmesh.messaging.data.dao.ConversationDao +import com.greybox.projectmesh.user.UserDao +import androidx.room.RoomDatabase + +/** + * JVM-only tests for MeshDatabase class shape. + * These do NOT spin up Room or touch Android runtime. + * + * We verify: + * - MeshDatabase is abstract + * - MeshDatabase extends RoomDatabase + * - Required DAO methods exist and have the correct return types + * + * Anything involving entities, queries, schema, and migrations belongs + * in src/androidTest with an in-memory RoomDatabase. + */ +class MeshDatabaseTest { + + @Test + fun meshDatabase_isAbstract_and_extendsRoomDatabase() { + val cls = MeshDatabase::class.java + + // Must be abstract + assertTrue("MeshDatabase must be abstract", + Modifier.isAbstract(cls.modifiers)) + + // Must extend androidx.room.RoomDatabase + assertTrue("MeshDatabase must extend RoomDatabase", + RoomDatabase::class.java.isAssignableFrom(cls)) + } + + @Test + fun meshDatabase_has_requiredDaoMethods_with_correctReturnTypes() { + val cls = MeshDatabase::class.java + + // messageDao(): MessageDao + val messageDaoMethod = cls.getMethod("messageDao") + assertEquals(MessageDao::class.java, messageDaoMethod.returnType) + + // userDao(): UserDao + val userDaoMethod = cls.getMethod("userDao") + assertEquals(UserDao::class.java, userDaoMethod.returnType) + + // conversationDao(): ConversationDao + val conversationDaoMethod = cls.getMethod("conversationDao") + assertEquals(ConversationDao::class.java, conversationDaoMethod.returnType) + } +} From 67360c0adb47992759f526479d3a9fe6eb84b9d1 Mon Sep 17 00:00:00 2001 From: Jai Patel <22jaipp@gmail.com> Date: Wed, 12 Nov 2025 15:58:41 -0700 Subject: [PATCH 3/9] Created Unit testing file specifically to test WifiConnection.kt --- .../projectmesh/debug/CrashHandlerTest.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/src/test/java/com/greybox/projectmesh/debug/CrashHandlerTest.kt diff --git a/app/src/test/java/com/greybox/projectmesh/debug/CrashHandlerTest.kt b/app/src/test/java/com/greybox/projectmesh/debug/CrashHandlerTest.kt new file mode 100644 index 000000000..f9a18bf26 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/debug/CrashHandlerTest.kt @@ -0,0 +1,88 @@ +package com.greybox.projectmesh.debug + +import com.google.gson.Gson +import org.junit.Assert.* +import org.junit.Test +import java.lang.Thread.UncaughtExceptionHandler + +/** + * JVM-only tests for CrashHandler. + * We DO NOT invoke Android runtime (no Context/Intent usage at runtime). + * All checks are reflection-based and safe for plain JVM. + */ +class CrashHandlerTest { + + /** + * CrashHandler must implement Thread.UncaughtExceptionHandler. + * (If someone removes/changes this, we catch it early.) + */ + @Test + fun crashHandler_implements_UncaughtExceptionHandler() { + assertTrue( + UncaughtExceptionHandler::class.java.isAssignableFrom(CrashHandler::class.java) + ) + } + + /** + * Verify the primary constructor shape: + * (Context, UncaughtExceptionHandler, Class<*>) + * We don't instantiate anything; we only look up the signature. + */ + @Test + fun crashHandler_has_expected_constructor_signature() { + val ctx = android.content.Context::class.java + val ueh = UncaughtExceptionHandler::class.java + val klass = Class::class.java + + // If the constructor is missing or signature changes, this throws NoSuchMethodException + val ctor = CrashHandler::class.java.getDeclaredConstructor(ctx, ueh, klass) + assertNotNull(ctor) + } + + /** + * Companion must expose: + * - init(Context, Class<*>) + * - getThrowableFromIntent(Intent): Throwable? + * We assert presence and parameter/return types by reflection. + */ + @Test + fun companion_has_init_and_getThrowableFromIntent_signatures() { + // Access the Kotlin "Companion" object using plain Java reflection. + val companionField = CrashHandler::class.java.getDeclaredField("Companion") + companionField.isAccessible = true + val companion = companionField.get(null) + ?: throw AssertionError("CrashHandler must have a companion object") + + val companionClass = companion::class.java + + // init(Context, Class<*>) + val ctx = android.content.Context::class.java + val klass = Class::class.java + val initMethod = companionClass.getMethod("init", ctx, klass) + assertEquals(Void.TYPE, initMethod.returnType) + + // getThrowableFromIntent(Intent): Throwable? + val intent = android.content.Intent::class.java + val getMethod = companionClass.getMethod("getThrowableFromIntent", intent) + assertTrue(Throwable::class.java.isAssignableFrom(getMethod.returnType)) + // Nullable return can't be asserted at runtime; we only check the declared type. + } + + /** + * Sanity: the Gson strategy (JSON round-trip) works in general. + * We DON'T use Throwable here because JDK 17+ blocks reflective access + * to its internal fields, which causes JsonIOException. + * Real Throwable JSON handling will be validated in instrumented tests. + */ + @Test + fun gson_can_roundtrip_simple_crash_payload() { + data class CrashPayload(val message: String) + + val gson = Gson() + val original = CrashPayload("crash-demo") + val json = gson.toJson(original) + val parsed = gson.fromJson(json, CrashPayload::class.java) + + assertEquals("crash-demo", parsed.message) + } +} From c28cc89eafccc7a9febf3cf4b16f282803d384a1 Mon Sep 17 00:00:00 2001 From: taimuradam Date: Wed, 12 Nov 2025 22:47:09 -0700 Subject: [PATCH 4/9] Remove redundant read override and add tests for InputStreamCounter Deleted the unnecessary override of read(ByteArray) in InputStreamCounter, relying on the existing read(ByteArray, Int, Int) implementation for byte counting. Added unit tests for InputStreamCounter to verify byte counting and closed state behavior. --- .gitignore | 2 + .../projectmesh/server/InputStreamCounter.kt | 7 -- .../server/InputStreamCounterTest.kt | 83 +++++++++++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt diff --git a/.gitignore b/.gitignore index 6e1af9e57..3e0c6f4d4 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,5 @@ kotlin-ide/ .idea/ .aider* .env +.vscode/settings.json +.idea/misc.xml diff --git a/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt b/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt index 256ba9422..df4821c7c 100644 --- a/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt +++ b/app/src/main/java/com/greybox/projectmesh/server/InputStreamCounter.kt @@ -22,13 +22,6 @@ class InputStreamCounter( } } - override fun read(b: ByteArray): Int { - return super.read(b).also { - if(it != -1) - bytesRead += it - } - } - override fun read(b: ByteArray, off: Int, len: Int): Int { return super.read(b, off, len).also { if(it != -1) diff --git a/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt b/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt new file mode 100644 index 000000000..2e6c71fdd --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt @@ -0,0 +1,83 @@ +package com.greybox.projectmesh.server + +import org.junit.Assert.* +import org.junit.Test +import java.io.ByteArrayInputStream + +class InputStreamCounterTest { + + @Test + fun readSingleBytes_countsAllBytes() { + val data = "hello world".toByteArray() + val input = ByteArrayInputStream(data) + val counter = InputStreamCounter(input) + + while (true) { + val result = counter.read() + if (result == -1) break + } + + assertEquals(data.size, counter.bytesRead) + assertFalse(counter.closed) + } + + @Test + fun readIntoBuffer_countsAllBytes() { + val data = ByteArray(4096) { it.toByte() } + val input = ByteArrayInputStream(data) + val counter = InputStreamCounter(input) + + val buffer = ByteArray(1024) + while (true) { + val n = counter.read(buffer) + if (n == -1) break + } + + assertEquals(data.size, counter.bytesRead) + } + + @Test + fun readWithOffset_countsAllBytes() { + val data = "abcdefghi".toByteArray() + val input = ByteArrayInputStream(data) + val counter = InputStreamCounter(input) + + val buffer = ByteArray(10) + while (true) { + val n = counter.read(buffer, 1, 4) + if (n == -1) break + } + + assertEquals(data.size, counter.bytesRead) + } + + @Test + fun bytesRead_doesNotIncreaseAfterEof() { + val data = "test".toByteArray() + val input = ByteArrayInputStream(data) + val counter = InputStreamCounter(input) + + val buffer = ByteArray(2) + while (counter.read(buffer) != -1) { + } + + val before = counter.bytesRead + val eofRead = counter.read(buffer) + + assertEquals(-1, eofRead) + assertEquals(before, counter.bytesRead) + } + + @Test + fun close_setsClosedFlag() { + val data = "xyz".toByteArray() + val input = ByteArrayInputStream(data) + val counter = InputStreamCounter(input) + + assertFalse(counter.closed) + + counter.close() + + assertTrue(counter.closed) + } +} \ No newline at end of file From 597de63520e7e5096cbcf18758f7c7e7774b1c3f Mon Sep 17 00:00:00 2001 From: taimuradam Date: Thu, 13 Nov 2025 03:21:45 -0700 Subject: [PATCH 5/9] Add ListExtensionTest and rename InputStreamCounterTest methods Introduced ListExtensionTest to cover updateItem extension function with various scenarios. Renamed test methods in InputStreamCounterTest for improved clarity and consistency. --- .../extension/ListExtensionTest.kt | 70 +++++++++++++++++++ .../server/InputStreamCounterTest.kt | 10 +-- 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt diff --git a/app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt b/app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt new file mode 100644 index 000000000..4ad24eec8 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt @@ -0,0 +1,70 @@ +package com.greybox.projectmesh.extension + +import org.junit.Assert.* +import org.junit.Test + +class ListExtensionTest { + + @Test + fun checkUpdateFirstItem() { + val list = listOf(1, 2, 3, 4) + val updated = list.updateItem( + condition = { it % 2 == 0 }, + function = { it * 10 } + ) + + assertEquals(listOf(1, 20, 3, 4), updated) + } + + @Test + fun checkOnlyFirstMatch() { + val list = listOf(2, 4, 6) + val updated = list.updateItem( + condition = { it % 2 == 0 }, + function = { it * 10 } + ) + + assertEquals(listOf(20, 4, 6), updated) + } + + @Test + fun checkNoMatch() { + val list = listOf(1, 3, 5) + val updated = list.updateItem( + condition = { it % 2 == 0 }, + function = { it * 10 } + ) + + assertSame(list, updated) + assertEquals(listOf(1, 3, 5), updated) + } + + @Test + fun checkEmptyList() { + val list = emptyList() + val updated = list.updateItem( + condition = { true }, + function = { it * 10 } + ) + + assertSame(list, updated) + assertTrue(updated.isEmpty()) + } + + @Test + fun checkCallingFunctionNoMatch() { + val list = listOf(1, 3, 5) + var called = false + + val updated = list.updateItem( + condition = { it % 2 == 0 }, + function = { + called = true + it * 10 + } + ) + + assertFalse(called) + assertSame(list, updated) + } +} diff --git a/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt b/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt index 2e6c71fdd..094151a6b 100644 --- a/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt +++ b/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt @@ -7,7 +7,7 @@ import java.io.ByteArrayInputStream class InputStreamCounterTest { @Test - fun readSingleBytes_countsAllBytes() { + fun countSingleBytes() { val data = "hello world".toByteArray() val input = ByteArrayInputStream(data) val counter = InputStreamCounter(input) @@ -22,7 +22,7 @@ class InputStreamCounterTest { } @Test - fun readIntoBuffer_countsAllBytes() { + fun readBufferBytes() { val data = ByteArray(4096) { it.toByte() } val input = ByteArrayInputStream(data) val counter = InputStreamCounter(input) @@ -37,7 +37,7 @@ class InputStreamCounterTest { } @Test - fun readWithOffset_countsAllBytes() { + fun countOffsetBytes() { val data = "abcdefghi".toByteArray() val input = ByteArrayInputStream(data) val counter = InputStreamCounter(input) @@ -52,7 +52,7 @@ class InputStreamCounterTest { } @Test - fun bytesRead_doesNotIncreaseAfterEof() { + fun checkEOF() { val data = "test".toByteArray() val input = ByteArrayInputStream(data) val counter = InputStreamCounter(input) @@ -69,7 +69,7 @@ class InputStreamCounterTest { } @Test - fun close_setsClosedFlag() { + fun checkClose() { val data = "xyz".toByteArray() val input = ByteArrayInputStream(data) val counter = InputStreamCounter(input) From d6ad974b73f36e94ad32fb372240f0f104a11586 Mon Sep 17 00:00:00 2001 From: taimuradam Date: Thu, 13 Nov 2025 11:12:19 -0700 Subject: [PATCH 6/9] Add unit tests for ConversationUtils Introduces ConversationUtilsTest to verify conversation ID creation logic, including handling of identical UUIDs and special cases for device UUIDs. --- .../messaging/utils/ConversationUtilsTest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt diff --git a/app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt b/app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt new file mode 100644 index 000000000..8e5b2470e --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt @@ -0,0 +1,34 @@ +package com.greybox.projectmesh.messaging.utils + +import org.junit.Assert.* +import org.junit.Test + +class ConversationUtilsTest { + + @Test + fun checkCreateConversationId() { + val id1 = ConversationUtils.createConversationId("a", "b") + val id2 = ConversationUtils.createConversationId("b", "a") + + assertEquals("a-b", id1) + assertEquals(id1, id2) + } + + @Test + fun checkIdenticalUuids() { + val id = ConversationUtils.createConversationId("same", "same") + assertEquals("same-same", id) + } + + @Test + fun checkSpecialCase() { + val id = ConversationUtils.createConversationId("anything", "test-device-uuid") + assertEquals("local-user-test-device-uuid", id) + } + + @Test + fun checkSecondSpecialCase() { + val id = ConversationUtils.createConversationId("anything", "offline-test-device-uuid") + assertEquals("local-user-offline-test-device-uuid", id) + } +} From d3250d32a2ce41a80645f99e5fe0cf9a5d9fb994 Mon Sep 17 00:00:00 2001 From: taimuradam Date: Thu, 13 Nov 2025 11:25:27 -0700 Subject: [PATCH 7/9] Add detailed comments to test classes Added descriptive block and section comments to ListExtensionTest, ConversationUtilsTest, and InputStreamCounterTest to clarify test coverage, intent, and expected behaviors for JVM-only extension and utility functions. Improves maintainability and understanding of test cases. --- .../extension/ListExtensionTest.kt | 27 ++++++++++++++++++- .../messaging/utils/ConversationUtilsTest.kt | 22 +++++++++++++++ .../server/InputStreamCounterTest.kt | 26 ++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt b/app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt index 4ad24eec8..e00d0f4ad 100644 --- a/app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt +++ b/app/src/test/java/com/greybox/projectmesh/extension/ListExtensionTest.kt @@ -3,8 +3,21 @@ package com.greybox.projectmesh.extension import org.junit.Assert.* import org.junit.Test +/** + * JVM-only tests for List.updateItem extension. + * + * Verifies: + * - first matching item is updated + * - only the first match is changed + * - lists with no match return the same instance + * - empty lists behave correctly + * - update function is not invoked when no match exists + */ class ListExtensionTest { + // -------------------------------------- + // First match: should update first match + // -------------------------------------- @Test fun checkUpdateFirstItem() { val list = listOf(1, 2, 3, 4) @@ -16,6 +29,9 @@ class ListExtensionTest { assertEquals(listOf(1, 20, 3, 4), updated) } + // --------------------------------------------------- + // Only first matching item should be transformed once + // --------------------------------------------------- @Test fun checkOnlyFirstMatch() { val list = listOf(2, 4, 6) @@ -27,6 +43,9 @@ class ListExtensionTest { assertEquals(listOf(20, 4, 6), updated) } + // ------------------------------------------------- + // No match: must return same list instance unchanged + // ------------------------------------------------- @Test fun checkNoMatch() { val list = listOf(1, 3, 5) @@ -39,6 +58,9 @@ class ListExtensionTest { assertEquals(listOf(1, 3, 5), updated) } + // --------------------------- + // Empty list stays unchanged + // --------------------------- @Test fun checkEmptyList() { val list = emptyList() @@ -51,6 +73,9 @@ class ListExtensionTest { assertTrue(updated.isEmpty()) } + // ------------------------------------------------------- + // No match: function should not be executed even once + // ------------------------------------------------------- @Test fun checkCallingFunctionNoMatch() { val list = listOf(1, 3, 5) @@ -67,4 +92,4 @@ class ListExtensionTest { assertFalse(called) assertSame(list, updated) } -} +} \ No newline at end of file diff --git a/app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt b/app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt index 8e5b2470e..d5370c5a1 100644 --- a/app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt +++ b/app/src/test/java/com/greybox/projectmesh/messaging/utils/ConversationUtilsTest.kt @@ -3,8 +3,20 @@ package com.greybox.projectmesh.messaging.utils import org.junit.Assert.* import org.junit.Test +/** + * JVM-only tests for ConversationUtils.createConversationId. + * + * Verifies: + * - stable ordering of UUID pairs + * - identical UUIDs produce expected ID + * - special-case handling for device UUIDs + * - offline device UUIDs follow same special rule + */ class ConversationUtilsTest { + // ---------------------------------------- + // Ordering: "a","b" should equal "b","a" + // ---------------------------------------- @Test fun checkCreateConversationId() { val id1 = ConversationUtils.createConversationId("a", "b") @@ -14,18 +26,28 @@ class ConversationUtilsTest { assertEquals(id1, id2) } + // -------------------------------------- + // Identical values should join naturally + // -------------------------------------- @Test fun checkIdenticalUuids() { val id = ConversationUtils.createConversationId("same", "same") assertEquals("same-same", id) } + // ----------------------------------------------------- + // Special rule: remote UUID "test-device-uuid" maps to + // "local-user-" + // ----------------------------------------------------- @Test fun checkSpecialCase() { val id = ConversationUtils.createConversationId("anything", "test-device-uuid") assertEquals("local-user-test-device-uuid", id) } + // ----------------------------------------------------- + // Offline device rule: mirrors test-device behavior + // ----------------------------------------------------- @Test fun checkSecondSpecialCase() { val id = ConversationUtils.createConversationId("anything", "offline-test-device-uuid") diff --git a/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt b/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt index 094151a6b..3e15809e2 100644 --- a/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt +++ b/app/src/test/java/com/greybox/projectmesh/server/InputStreamCounterTest.kt @@ -4,8 +4,21 @@ import org.junit.Assert.* import org.junit.Test import java.io.ByteArrayInputStream +/** + * JVM-only tests for InputStreamCounter. + * + * Verifies: + * - single-byte reads + * - buffered reads + * - offset reads + * - EOF behavior + * - close() flag + */ class InputStreamCounterTest { + // ----------------------------------------- + // Single-byte read: should count each byte + // ----------------------------------------- @Test fun countSingleBytes() { val data = "hello world".toByteArray() @@ -21,6 +34,9 @@ class InputStreamCounterTest { assertFalse(counter.closed) } + // ---------------------------------------------------------- + // Buffered read: reading in chunks should count total bytes + // ---------------------------------------------------------- @Test fun readBufferBytes() { val data = ByteArray(4096) { it.toByte() } @@ -36,6 +52,9 @@ class InputStreamCounterTest { assertEquals(data.size, counter.bytesRead) } + // ---------------------------------------------------------------- + // Offset read: read(buffer, off, len) must still count accurately + // ---------------------------------------------------------------- @Test fun countOffsetBytes() { val data = "abcdefghi".toByteArray() @@ -51,6 +70,9 @@ class InputStreamCounterTest { assertEquals(data.size, counter.bytesRead) } + // --------------------------------------------------------- + // EOF behavior: read after EOF should return -1 and not add + // --------------------------------------------------------- @Test fun checkEOF() { val data = "test".toByteArray() @@ -59,6 +81,7 @@ class InputStreamCounterTest { val buffer = ByteArray(2) while (counter.read(buffer) != -1) { + // consume all data } val before = counter.bytesRead @@ -68,6 +91,9 @@ class InputStreamCounterTest { assertEquals(before, counter.bytesRead) } + // -------------------------- + // close(): should set flag + // -------------------------- @Test fun checkClose() { val data = "xyz".toByteArray() From 31f7046f160e11ec9477e97aa145325bb655b4ea Mon Sep 17 00:00:00 2001 From: Jai Patel <22jaipp@gmail.com> Date: Sat, 22 Nov 2025 19:01:43 -0700 Subject: [PATCH 8/9] Created Unit more testing files for multiple files --- .../debug/CrashScreenActivityTest.kt | 44 +++++++++ .../projectmesh/navigation/BottomNavTest.kt | 90 +++++++++++++++++++ .../navigation/NavigationItemTest.kt | 45 ++++++++++ .../projectmesh/ui/theme/ThemeLayerTest.kt | 77 ++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 app/src/test/java/com/greybox/projectmesh/debug/CrashScreenActivityTest.kt create mode 100644 app/src/test/java/com/greybox/projectmesh/navigation/BottomNavTest.kt create mode 100644 app/src/test/java/com/greybox/projectmesh/navigation/NavigationItemTest.kt create mode 100644 app/src/test/java/com/greybox/projectmesh/ui/theme/ThemeLayerTest.kt diff --git a/app/src/test/java/com/greybox/projectmesh/debug/CrashScreenActivityTest.kt b/app/src/test/java/com/greybox/projectmesh/debug/CrashScreenActivityTest.kt new file mode 100644 index 000000000..4e09e27f2 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/debug/CrashScreenActivityTest.kt @@ -0,0 +1,44 @@ +package com.greybox.projectmesh.debug + +import android.content.Intent +import com.google.gson.Gson +import org.junit.Assert.* +import org.junit.Test + +class CrashScreenActivityTest { + + @Test + fun getThrowableFromIntent_returnsThrowable_whenValidJsonProvided() { + // SAFE: minimal JSON, avoids Gson reflection into private fields. + val json = """{"detailMessage":"boom-crash"}""" + + val intent = Intent().apply { + putExtra("CrashData", json) + } + + val parsed = CrashHandler.getThrowableFromIntent(intent) + + assertNotNull(parsed) + assertEquals("boom-crash", parsed?.message) + } + + @Test + fun getThrowableFromIntent_returnsNull_whenInvalidJsonProvided() { + val intent = Intent().apply { + putExtra("CrashData", "{invalid-json}") + } + + val parsed = CrashHandler.getThrowableFromIntent(intent) + + assertNull(parsed) + } + + @Test + fun getThrowableFromIntent_returnsNull_whenNoCrashDataProvided() { + val intent = Intent() + + val parsed = CrashHandler.getThrowableFromIntent(intent) + + assertNull(parsed) + } +} diff --git a/app/src/test/java/com/greybox/projectmesh/navigation/BottomNavTest.kt b/app/src/test/java/com/greybox/projectmesh/navigation/BottomNavTest.kt new file mode 100644 index 000000000..6fad19e40 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/navigation/BottomNavTest.kt @@ -0,0 +1,90 @@ +package com.greybox.projectmesh.navigation + +import org.junit.Assert.* +import org.junit.Test + +/** + * JVM unit tests for the BottomNavItem sealed class. + * + * We only check pure data: + * - each object has the expected route and title + * - all routes are unique + * - icons are present (non-null references) + * + * No Compose runtime or Android APIs are used here, so this is safe + * as a local unit test. + */ +class BottomNavItemTest { + + private val allItems = listOf( + BottomNavItem.Home, + BottomNavItem.Network, + BottomNavItem.Send, + BottomNavItem.Receive, + BottomNavItem.Log, + BottomNavItem.Settings, + BottomNavItem.Chat + ) + + @Test + fun allItems_haveExpectedRoutesAndTitles() { + // route, title pairs we expect + val expected = mapOf( + BottomNavItem.Home to ("home" to "Home"), + BottomNavItem.Network to ("network" to "Network"), + BottomNavItem.Send to ("send" to "Send"), + BottomNavItem.Receive to ("receive" to "Receive"), + BottomNavItem.Log to ("log" to "Log"), + BottomNavItem.Settings to ("settings" to "Settings"), + BottomNavItem.Chat to ("chat" to "Chat"), + ) + + for (item in allItems) { + val (expRoute, expTitle) = expected[item] + ?: error("Missing expectations for $item") + + assertEquals("Wrong route for ${item::class.simpleName}", expRoute, item.route) + assertEquals("Wrong title for ${item::class.simpleName}", expTitle, item.title) + } + } + + @Test + fun allItems_haveNonNullIcons() { + allItems.forEach { item -> + assertNotNull( + "Icon must not be null for ${item::class.simpleName}", + item.icon + ) + } + } + + @Test + fun routes_areUniqueAcrossAllItems() { + val routes = allItems.map { it.route } + val distinctRoutes = routes.toSet() + + assertEquals( + "Each BottomNavItem should use a unique route", + distinctRoutes.size, + routes.size + ) + } + + @Test + fun sealedHierarchy_containsExactlyExpectedItems() { + // This guards against someone adding a new object without updating tests. + val classes = allItems.map { it::class.simpleName }.toSet() + + val expectedNames = setOf( + "Home", + "Network", + "Send", + "Receive", + "Log", + "Settings", + "Chat" + ) + + assertEquals(expectedNames, classes) + } +} diff --git a/app/src/test/java/com/greybox/projectmesh/navigation/NavigationItemTest.kt b/app/src/test/java/com/greybox/projectmesh/navigation/NavigationItemTest.kt new file mode 100644 index 000000000..d82e1ced5 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/navigation/NavigationItemTest.kt @@ -0,0 +1,45 @@ +package com.greybox.projectmesh.navigation + +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import org.junit.Assert.* +import org.junit.Test + +/** + * JVM unit tests for the NavigationItem data class. + * + * NOTE: + * - Composables & NavController CANNOT be JVM tested. + * - We only verify that NavigationItem behaves as a proper + * Kotlin data class (copy, equals, hashCode). + */ +class NavigationItemTest { + + // Just need some non-null ImageVector instance + private val dummyIcon: ImageVector = ImageVector.Builder( + defaultWidth = 24.dp, // <-- Dp, not Float + defaultHeight = 24.dp, // <-- Dp, not Float + viewportWidth = 24f, + viewportHeight = 24f + ).build() + + @Test + fun navigationItem_copyEqualsHashCodeCorrect() { + val original = NavigationItem( + route = "home", + label = "Home", + icon = dummyIcon + ) + + val copy = original.copy() + val modified = original.copy(route = "different") + + // Same data → equal + assertEquals(original, copy) + assertEquals(original.hashCode(), copy.hashCode()) + + // Different route → not equal + assertNotEquals(original, modified) + assertEquals("different", modified.route) + } +} diff --git a/app/src/test/java/com/greybox/projectmesh/ui/theme/ThemeLayerTest.kt b/app/src/test/java/com/greybox/projectmesh/ui/theme/ThemeLayerTest.kt new file mode 100644 index 000000000..e1cd7c5ff --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/ui/theme/ThemeLayerTest.kt @@ -0,0 +1,77 @@ +package com.greybox.projectmesh.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import org.junit.Assert.* +import org.junit.Test + +/** + * JVM unit tests for the theme layer: + * + * - Color constants in Color.kt + * - AppTheme enum in Theme.kt + * - Typography definition in Type.kt + * + * NOTE: + * - We do NOT run the ProjectMeshTheme composable here, + * because that requires a real Compose runtime / Android. + * - Instrumented tests will check the actual MaterialTheme + * behavior (dark/light, system theme, etc.). + */ +class ThemeLayerTest { + + // ----------------------------- + // Color constants (Color.kt) + // ----------------------------- + + @Test + fun colors_haveExpectedArgbValues() { + // Light variants + assertEquals(Color(0xFFD0BCFF), Purple80) + assertEquals(Color(0xFFCCC2DC), PurpleGrey80) + assertEquals(Color(0xFFEFB8C8), Pink80) + + // Darker variants + assertEquals(Color(0xFF6650A4), Purple40) + assertEquals(Color(0xFF625B71), PurpleGrey40) + assertEquals(Color(0xFF7D5260), Pink40) + } + + // ----------------------------- + // AppTheme enum (Theme.kt) + // ----------------------------- + + @Test + fun appTheme_containsExpectedValuesInOrder() { + val values = enumValues().toList() + + assertEquals(3, values.size) + assertEquals(AppTheme.SYSTEM, values[0]) + assertEquals(AppTheme.LIGHT, values[1]) + assertEquals(AppTheme.DARK, values[2]) + + val names = values.map { it.name }.toSet() + assertEquals(setOf("SYSTEM", "LIGHT", "DARK"), names) + } + + // ----------------------------- + // Typography (Type.kt) + // ----------------------------- + + @Test + fun typography_bodyLarge_hasExpectedDefaults() { + // Typography is the object defined in Type.kt + val body = Typography.bodyLarge + + // Font family & weight + assertEquals(FontFamily.Default, body.fontFamily) + assertEquals(FontWeight.Normal, body.fontWeight) + + // Sizes (we compare the .value floats for simplicity) + assertEquals(16f, body.fontSize.value, 0.0f) + assertEquals(24f, body.lineHeight.value, 0.0f) + assertEquals(0.5f, body.letterSpacing.value, 0.0f) + } +} From b8aad94a7aec3ba3d2c6c77729b06e865a127290 Mon Sep 17 00:00:00 2001 From: taimuradam Date: Mon, 24 Nov 2025 18:07:25 -0700 Subject: [PATCH 9/9] Added 4 new test files Added tests for FileEncoder, Logger, MessageMigrationUtils and MessageUtils --- .idea/misc.xml | 2 + .../messaging/data/entities/FileEncoder.kt | 84 ++++++++++--------- .../projectmesh/messaging/utils/Logger.kt | 27 ++++-- .../messaging/utils/MessageMigrationUtils.kt | 2 +- .../data/entities/FileEncoderTest.kt | 49 +++++++++++ .../projectmesh/messaging/utils/LoggerTest.kt | 31 +++++++ .../utils/MessageMigrationUtilsTest.kt | 52 ++++++++++++ .../messaging/utils/MessageUtilsTest.kt | 41 +++++++++ 8 files changed, 237 insertions(+), 51 deletions(-) create mode 100644 app/src/test/java/com/greybox/projectmesh/messaging/data/entities/FileEncoderTest.kt create mode 100644 app/src/test/java/com/greybox/projectmesh/messaging/utils/LoggerTest.kt create mode 100644 app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtilsTest.kt create mode 100644 app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageUtilsTest.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 0bd3ec25a..74dd639e4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,6 @@ + + diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt index 25bcfbf44..f21a25e9e 100644 --- a/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt +++ b/app/src/main/java/com/greybox/projectmesh/messaging/data/entities/FileEncoder.kt @@ -53,68 +53,70 @@ import java.net.URL import java.net.URLConnection import java.net.URLDecoder -//Use this to encode files not just images -//Needs to be tested sometime -//Can I modify this so that Http transfer does the majority of the encoding? -class FileEncoder {//Made by Craig. Encodes via base64. +class FileEncoder { @OptIn(ExperimentalEncodingApi::class) - fun encodebase64(ctxt: Context, inputuri: Uri): String?{ - try { + fun encodebase64(ctxt: Context, inputuri: Uri): String? { + return try { val encodedstrm: InputStream? = ctxt.contentResolver.openInputStream(inputuri) val bytes = encodedstrm?.readBytes() encodedstrm?.close() - return if (bytes != null) { - Base64.encode(bytes) - } else { - "Cannot encode file" - } - } catch(e: Exception){ + encodeBytesBase64(bytes) + } catch (e: Exception) { e.printStackTrace() - return "Cannot encode file"} + "Cannot encode file" + } + } + @OptIn(ExperimentalEncodingApi::class) + internal fun encodeBytesBase64(bytes: ByteArray?): String? { + return if (bytes != null) { + Base64.encode(bytes) + } else { + "Cannot encode file" + } } - @OptIn(ExperimentalEncodingApi::class)//Made by Craig - fun decodeBase64(inputbase64:String, output: File): File{//Decodes to a file. Uses base64 + @OptIn(ExperimentalEncodingApi::class) + fun decodeBase64(inputbase64: String, output: File): File { val decodedfilebytes = Base64.decode(inputbase64) val decodedstrm = FileOutputStream(output) decodedstrm.write(decodedfilebytes) decodedstrm.close() return output } - fun sendImage(imageURI: Uri?, tgtaddress: InetAddress, tgtport:Int, appctxt: Context): Boolean{//Testing sending images - try{//we can utilize this if we opt not to use JSON - if(imageURI != null){ - val fp = encodebase64(appctxt, imageURI)//encodes file to base64 - if(!fp.equals("Cannot encode file")) { - val efp = URLEncoder.encode(fp, "UTF-8")//ensures that the file URI is utf-8 encoded - val connection = - URL("http://${tgtaddress.hostAddress}:${tgtport}/upload?file=$efp").openConnection() as HttpURLConnection - val request = "POST"//Specifies the request as a POST - connection.doOutput = true - connection.requestMethod = request - connection.setChunkedStreamingMode(0) - val instream = appctxt.contentResolver.openInputStream(imageURI) - val outstream = connection.outputStream - val readingbuffer = ByteArray(1024) - var finishedreading: Int - while (instream?.read(readingbuffer).also { finishedreading = it!! } != -1) { - outstream.write(readingbuffer, 0, finishedreading) - } - outstream.close() - instream?.close() + fun sendImage(imageURI: Uri?, tgtaddress: InetAddress, tgtport: Int, appctxt: Context): Boolean { + try { + if (imageURI != null) { + val fp = encodebase64(appctxt, imageURI) + if (!fp.equals("Cannot encode file")) { + val efp = URLEncoder.encode(fp, "UTF-8") + val connection = + URL("http://${tgtaddress.hostAddress}:${tgtport}/upload?file=$efp").openConnection() as HttpURLConnection + val request = "POST" + connection.doOutput = true + connection.requestMethod = request + connection.setChunkedStreamingMode(0) + val instream = appctxt.contentResolver.openInputStream(imageURI) + val outstream = connection.outputStream + val readingbuffer = ByteArray(1024) + var finishedreading: Int + while (instream?.read(readingbuffer).also { finishedreading = it!! } != -1) { + outstream.write(readingbuffer, 0, finishedreading) + } + outstream.close() + instream?.close() + } else { + return false + } } else { return false - }} else { - return false } - } - catch(e: Exception){ + } catch (e: Exception) { e.printStackTrace() return false } return true } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt b/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt index 9000bc113..871374a87 100644 --- a/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt +++ b/app/src/main/java/com/greybox/projectmesh/messaging/utils/Logger.kt @@ -8,43 +8,52 @@ import android.util.Log */ object Logger { + internal const val TAG_PREFIX = "MeshChat_" private const val LOGGING_ENABLED = true - private const val TAG_PREFIX = "MeshChat_" + + internal fun buildTag(tag: String): String { + return "$TAG_PREFIX$tag" + } + + internal fun buildCriticalTag(tag: String): String { + return "${TAG_PREFIX}${tag}_CRITICAL" + } fun d(tag: String, message: String) { if (LOGGING_ENABLED) { - Log.d("$TAG_PREFIX$tag", message) + Log.d(buildTag(tag), message) } } fun i(tag: String, message: String) { if (LOGGING_ENABLED) { - Log.i("$TAG_PREFIX$tag", message) + Log.i(buildTag(tag), message) } } fun w(tag: String, message: String) { if (LOGGING_ENABLED) { - Log.w("$TAG_PREFIX$tag", message) + Log.w(buildTag(tag), message) } } fun e(tag: String, message: String, throwable: Throwable? = null) { if (LOGGING_ENABLED) { if (throwable != null) { - Log.e("$TAG_PREFIX$tag", message, throwable) + Log.e(buildTag(tag), message, throwable) } else { - Log.e("$TAG_PREFIX$tag", message) + Log.e(buildTag(tag), message) } } } // Log important events that should be visible even in production fun critical(tag: String, message: String, throwable: Throwable? = null) { + val criticalTag = buildCriticalTag(tag) if (throwable != null) { - Log.e("$TAG_PREFIX${tag}_CRITICAL", message, throwable) + Log.e(criticalTag, message, throwable) } else { - Log.e("$TAG_PREFIX${tag}_CRITICAL", message) + Log.e(criticalTag, message) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt index a2bb342f4..22e10dcdf 100644 --- a/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt +++ b/app/src/main/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtils.kt @@ -93,7 +93,7 @@ class MessageMigrationUtils( } } - private fun createConversationId(uuid1: String, uuid2: String): String { + internal fun createConversationId(uuid1: String, uuid2: String): String { // Special cases for test devices if (uuid2 == "test-device-uuid") { return "local-user-test-device-uuid" diff --git a/app/src/test/java/com/greybox/projectmesh/messaging/data/entities/FileEncoderTest.kt b/app/src/test/java/com/greybox/projectmesh/messaging/data/entities/FileEncoderTest.kt new file mode 100644 index 000000000..1cef20f3c --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/messaging/data/entities/FileEncoderTest.kt @@ -0,0 +1,49 @@ +package com.greybox.projectmesh.messaging.data.entities + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File + +class FileEncoderTest { + + private val encoder = FileEncoder() + + @Test + fun encodeBytesBase64_encodesNonNullBytes() { + val original = "hello".toByteArray() + val result = encoder.encodeBytesBase64(original) + assertEquals("aGVsbG8=", result) + } + + @Test + fun encodeBytesBase64_returnsErrorMessageForNullBytes() { + val result = encoder.encodeBytesBase64(null) + assertEquals("Cannot encode file", result) + } + + @Test + fun decodeBase64_writesDecodedBytesToFile() { + val base64 = "aGVsbG8=" + val tempFile = File.createTempFile("fileencoder_test", ".bin") + tempFile.deleteOnExit() + + encoder.decodeBase64(base64, tempFile) + + val content = tempFile.readBytes() + assertArrayEquals("hello".toByteArray(), content) + } + + @Test + fun decodeBase64_overwritesExistingFileContent() { + val tempFile = File.createTempFile("fileencoder_overwrite_test", ".bin") + tempFile.writeText("old-content") + tempFile.deleteOnExit() + + val base64 = "aGVsbG8=" + encoder.decodeBase64(base64, tempFile) + + val content = tempFile.readBytes() + assertArrayEquals("hello".toByteArray(), content) + } +} diff --git a/app/src/test/java/com/greybox/projectmesh/messaging/utils/LoggerTest.kt b/app/src/test/java/com/greybox/projectmesh/messaging/utils/LoggerTest.kt new file mode 100644 index 000000000..cef18e398 --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/messaging/utils/LoggerTest.kt @@ -0,0 +1,31 @@ +package com.greybox.projectmesh.utils + +import org.junit.Assert.assertEquals +import org.junit.Test + +class LoggerTest { + + @Test + fun buildTag_prefixesWithMeshChat() { + val result = Logger.buildTag("ChatScreen") + assertEquals("MeshChat_ChatScreen", result) + } + + @Test + fun buildTag_handlesEmptyTag() { + val result = Logger.buildTag("") + assertEquals("MeshChat_", result) + } + + @Test + fun buildCriticalTag_appendsCriticalSuffix() { + val result = Logger.buildCriticalTag("Network") + assertEquals("MeshChat_Network_CRITICAL", result) + } + + @Test + fun buildCriticalTag_handlesEmptyTag() { + val result = Logger.buildCriticalTag("") + assertEquals("MeshChat__CRITICAL", result) + } +} diff --git a/app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtilsTest.kt b/app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtilsTest.kt new file mode 100644 index 000000000..9dde1d24b --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageMigrationUtilsTest.kt @@ -0,0 +1,52 @@ +package com.greybox.projectmesh.messaging.utils + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.kodein.di.DI + +class MessageMigrationUtilsTest { + + private val di = DI {} + private val utils = MessageMigrationUtils(di) + + @Test + fun createConversationId_sortsNormalUuids() { + val uuid1 = "b-uuid" + val uuid2 = "a-uuid" + + val result = utils.createConversationId(uuid1, uuid2) + + assertEquals("a-uuid-b-uuid", result) + } + + @Test + fun createConversationId_handlesTestDeviceUuid() { + val uuid1 = "some-other-uuid" + val uuid2 = "test-device-uuid" + + val result = utils.createConversationId(uuid1, uuid2) + + assertEquals("local-user-test-device-uuid", result) + } + + @Test + fun createConversationId_handlesOfflineTestDeviceUuid() { + val uuid1 = "some-other-uuid" + val uuid2 = "offline-test-device-uuid" + + val result = utils.createConversationId(uuid1, uuid2) + + assertEquals("local-user-offline-test-device-uuid", result) + } + + @Test + fun createConversationId_isDeterministicForSamePair() { + val uuidA = "1111-aaaa" + val uuidB = "2222-bbbb" + + val id1 = utils.createConversationId(uuidA, uuidB) + val id2 = utils.createConversationId(uuidB, uuidA) + + assertEquals(id1, id2) + } +} diff --git a/app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageUtilsTest.kt b/app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageUtilsTest.kt new file mode 100644 index 000000000..f69ad567c --- /dev/null +++ b/app/src/test/java/com/greybox/projectmesh/messaging/utils/MessageUtilsTest.kt @@ -0,0 +1,41 @@ +package com.greybox.projectmesh.messaging.utils + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.TimeZone + +class MessageUtilsTest { + + @Test + fun formatTimestamp_formatsToHoursAndMinutesInUtc() { + val originalTimeZone = TimeZone.getDefault() + try { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + val timestamp = 0L + val result = MessageUtils.formatTimestamp(timestamp) + assertEquals("00:00", result) + } finally { + TimeZone.setDefault(originalTimeZone) + } + } + + @Test + fun generateChatId_isOrderIndependent() { + val id1 = MessageUtils.generateChatId("alice", "bob") + val id2 = MessageUtils.generateChatId("bob", "alice") + assertEquals("alice-bob", id1) + assertEquals(id1, id2) + } + + @Test + fun generateChatId_handlesSameUser() { + val id = MessageUtils.generateChatId("alice", "alice") + assertEquals("alice-alice", id) + } + + @Test + fun generateChatId_isCaseSensitive() { + val id = MessageUtils.generateChatId("Alice", "alice") + assertEquals("Alice-alice", id) + } +}