feat: add pairing / connection string parsing & verify with unit tests
This commit is contained in:
1 parent
bfa959ed05
commit
d4328d0fb2
3 files changed
+132
No files matched your search
@@ -129,6 +129,27 @@ fun ConnectScreen(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var pairingString by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = pairingString,
|
||||||
|
onValueChange = {
|
||||||
|
pairingString = it
|
||||||
|
if (it.isNotBlank()) {
|
||||||
|
viewModel.onPairingString(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text("Pairing Link / Connection String") },
|
||||||
|
placeholder = { Text("Paste hermes://... or Base64 config") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// Token field
|
// Token field
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.token,
|
value = state.token,
|
||||||
|
|||||||
@@ -116,4 +116,63 @@ class ConnectViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onPairingString(value: String) {
|
||||||
|
val trimmed = value.trim()
|
||||||
|
if (trimmed.isBlank()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (trimmed.startsWith("hermes://connect?", ignoreCase = true)) {
|
||||||
|
val uri = android.net.Uri.parse(trimmed)
|
||||||
|
val host = uri.getQueryParameter("host")
|
||||||
|
val port = uri.getQueryParameter("port")
|
||||||
|
val token = uri.getQueryParameter("token")
|
||||||
|
if (host != null && port != null && token != null) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
token = token,
|
||||||
|
errorMessage = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
connect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try decoding as Base64 JSON
|
||||||
|
try {
|
||||||
|
val decodedBytes = android.util.Base64.decode(trimmed, android.util.Base64.DEFAULT)
|
||||||
|
val decodedString = String(decodedBytes, Charsets.UTF_8)
|
||||||
|
if (decodedString.startsWith("{") && decodedString.endsWith("}")) {
|
||||||
|
val json =
|
||||||
|
com.google.gson.JsonParser
|
||||||
|
.parseString(decodedString)
|
||||||
|
.asJsonObject
|
||||||
|
val host = json.get("host")?.asString
|
||||||
|
val port = json.get("port")?.asInt?.toString() ?: json.get("port")?.asString
|
||||||
|
val token = json.get("token")?.asString
|
||||||
|
if (host != null && port != null && token != null) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
token = token,
|
||||||
|
errorMessage = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
connect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore base64 decode failures, fallback to setting raw token
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.update { it.copy(token = trimmed, errorMessage = null) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "Failed to parse pairing string: ${e.message}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,4 +980,56 @@ class E2eIntegrationTest {
|
|||||||
coVerify { mockApiService.updateProfileModel("default", any()) }
|
coVerify { mockApiService.updateProfileModel("default", any()) }
|
||||||
assertEquals("Successfully set model to llama3", viewModel.uiState.value.toastMessage)
|
assertEquals("Successfully set model to llama3", viewModel.uiState.value.toastMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConnectViewModel_onPairingString_urlFormat() =
|
||||||
|
runTest {
|
||||||
|
mockkStatic(android.net.Uri::class)
|
||||||
|
val mockUri = mockk<android.net.Uri>()
|
||||||
|
every { android.net.Uri.parse("hermes://connect?host=192.168.1.5&port=9119&token=TEST_TOKEN") } returns
|
||||||
|
mockUri
|
||||||
|
every { mockUri.getQueryParameter("host") } returns "192.168.1.5"
|
||||||
|
every { mockUri.getQueryParameter("port") } returns "9119"
|
||||||
|
every { mockUri.getQueryParameter("token") } returns "TEST_TOKEN"
|
||||||
|
|
||||||
|
val statusResponse = mockk<StatusResponse>()
|
||||||
|
coEvery { mockApiService.getStatus() } returns Response.success(statusResponse)
|
||||||
|
|
||||||
|
val viewModel = ConnectViewModel()
|
||||||
|
viewModel.onPairingString("hermes://connect?host=192.168.1.5&port=9119&token=TEST_TOKEN")
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals("192.168.1.5", viewModel.uiState.value.host)
|
||||||
|
assertEquals("9119", viewModel.uiState.value.port)
|
||||||
|
assertEquals("TEST_TOKEN", viewModel.uiState.value.token)
|
||||||
|
assertTrue(viewModel.uiState.value.connectionSuccess)
|
||||||
|
|
||||||
|
unmockkStatic(android.net.Uri::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConnectViewModel_onPairingString_base64Format() =
|
||||||
|
runTest {
|
||||||
|
mockkStatic(android.util.Base64::class)
|
||||||
|
val base64Str = "eyJob3N0IjoiMTkyLjE2OC4xLjUiLCJwb3J0Ijo5MTE5LCJ0b2tlbiI6IlRFU1RfVE9LRU4ifQ=="
|
||||||
|
every { android.util.Base64.decode(base64Str, any()) } answers {
|
||||||
|
java.util.Base64
|
||||||
|
.getDecoder()
|
||||||
|
.decode(base64Str)
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusResponse = mockk<StatusResponse>()
|
||||||
|
coEvery { mockApiService.getStatus() } returns Response.success(statusResponse)
|
||||||
|
|
||||||
|
val viewModel = ConnectViewModel()
|
||||||
|
viewModel.onPairingString(base64Str)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals("192.168.1.5", viewModel.uiState.value.host)
|
||||||
|
assertEquals("9119", viewModel.uiState.value.port)
|
||||||
|
assertEquals("TEST_TOKEN", viewModel.uiState.value.token)
|
||||||
|
assertTrue(viewModel.uiState.value.connectionSuccess)
|
||||||
|
|
||||||
|
unmockkStatic(android.util.Base64::class)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user