diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index f2edaad9..1b605a49 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -214,7 +214,7 @@ fun cleanSocketName(raw: String): String = raw.removePrefix("as_").replace(Regex("_v[46]_(tcp|udp)$"), " $1") /** Stateless JNI/JSON adapter. Снимки применяются целиком: null означает отказ, пустой список — корректный пустой снимок. */ -class ChatRepository { +class ChatRepository(private val reportFailure: (String) -> Unit = { LogManager.addLog("ERROR", "Repository", it) }) { fun getChannels(): List? { val list = mutableListOf() try { @@ -231,7 +231,7 @@ class ChatRepository { )) } } catch (e: Exception) { - LogManager.addLog("ERROR", "Repository", "Channel snapshot failed: $e"); return null + reportFailure("Channel snapshot failed: $e"); return null } return list } @@ -243,7 +243,7 @@ class ChatRepository { val obj = arr.getJSONObject(i) if (obj.getString("id") == channelId) return obj.optInt("peersOnline", 0) } - } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Peer count failed channel=$channelId: $e") } + } catch (e: Exception) { reportFailure("Peer count failed channel=$channelId: $e") } return 0 } @@ -369,7 +369,7 @@ class ChatRepository { )) } } catch (e: Exception) { - LogManager.addLog("ERROR", "Repository", "Message snapshot failed channel=$channelId anchor=$anchor: $e"); return null + reportFailure("Message snapshot failed channel=$channelId anchor=$anchor: $e"); return null } return list } @@ -419,7 +419,7 @@ class ChatRepository { )) } } catch (e: Exception) { - LogManager.addLog("ERROR", "Repository", "Member snapshot failed channel=$channelId: $e"); return emptyList() + reportFailure("Member snapshot failed channel=$channelId: $e"); return emptyList() } return list } @@ -457,7 +457,7 @@ class ChatRepository { inviterId = if (obj.isNull("inviter_id")) null else obj.getLong("inviter_id"), inviterName = obj.optString("inviter_name", "") ) - } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Member detail failed channel=$channelId node=$nodeId: $e") } + } catch (e: Exception) { reportFailure("Member detail failed channel=$channelId node=$nodeId: $e") } return null } @@ -505,7 +505,7 @@ class ChatRepository { linksUp = obj.optInt("lu", 0), links = links ) - } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Member links failed node=$nodeId: $e") } + } catch (e: Exception) { reportFailure("Member links failed node=$nodeId: $e") } return MemberLinks() } @@ -535,7 +535,7 @@ class ChatRepository { } list.sortWith(compareBy { it.ifName }.thenBy { it.bindIp }.thenBy { it.protocol }.thenBy { it.port }) return list - } catch (e: Exception) { LogManager.addLog("ERROR", "Repository", "Socket snapshot parse failed: $e") } + } catch (e: Exception) { reportFailure("Socket snapshot parse failed: $e") } return null } @@ -563,7 +563,7 @@ class ChatRepository { )) } } catch (e: Exception) { - LogManager.addLog("ERROR", "Repository", "PM conversation snapshot failed: $e"); return null + reportFailure("PM conversation snapshot failed: $e"); return null } return list } @@ -612,7 +612,7 @@ class ChatRepository { )) } } catch (e: Exception) { - LogManager.addLog("ERROR", "Repository", "PM message snapshot failed: $e"); return null + reportFailure("PM message snapshot failed: $e"); return null } return list } diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/ChatRepositorySnapshotTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/ChatRepositorySnapshotTest.kt new file mode 100644 index 00000000..26f916ea --- /dev/null +++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/ChatRepositorySnapshotTest.kt @@ -0,0 +1,36 @@ +package com.utun.chat.data + +import org.junit.Assert.* +import org.junit.Test + +/** Повреждение второй строки не должно публиковать первый элемент как полный снимок. */ +class ChatRepositorySnapshotTest { + private val errors = mutableListOf() + private val repo = ChatRepository { errors.add(it) } + + @Test fun conversationParseFailureDiffersFromAValidEmptySnapshotAndAllowsRetry() { + val row = """{"conv_id":"123","peer_node_id":4,"peer_name":"Узел 😀","generation":2}""" + assertNull(repo.parseDmConversations("[$row,{}]")) + assertTrue(errors.single().contains("PM conversation snapshot failed")) + assertEquals(emptyList(), repo.parseDmConversations("[]")) + val restored = repo.parseDmConversations("[$row]")!!.single() + assertEquals("Узел 😀", restored.peerName); assertEquals(2L, restored.generation) + } + + @Test fun invalidInviteCannotPublishAPartialMessageHistory() { + val row = """{"seq":1,"author":"4","data":"Текст 😀","ts":50}""" + assertNull(repo.parseDmMessages("[$row,{\"ct\":\"invite\",\"tags\":{}}]")) + assertTrue(errors.single().contains("PM message snapshot failed")) + assertEquals(emptyList(), repo.parseDmMessages("[]")) + assertEquals("Текст 😀", repo.parseDmMessages("[$row]")!!.single().text) + } + + @Test fun missingRequiredNatFieldsDoNotClearOrTruncateTheSocketSnapshot() { + val row = """{"si":1,"nm":"as_eth_v4_udp","nmapping":1,"ninbound":1,"nsource":1,"external":"","externalPort":0}""" + val before = repo.parseLocalSockets("[$row]")!! + assertNull(repo.parseLocalSockets("[$row,{}]")) + assertTrue(errors.single().contains("Socket snapshot parse failed")) + assertEquals(emptyList(), repo.parseLocalSockets("[]")) + assertEquals(before, repo.parseLocalSockets("[$row]")) + } +}