This commit is contained in:
23 files changed
+2224
-53
No files matched your search
@@ -508,6 +508,151 @@ class EventParserTest {
|
||||
assertEquals(false, approval.allowPermanent)
|
||||
}
|
||||
|
||||
// ── Vault prompt events (issue #1090) ───────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseVaultUnlockRequest_andExpire() {
|
||||
val reqResponse =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "vault.unlock.request",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to
|
||||
mapOf(
|
||||
"request_id" to "req-unlock-1",
|
||||
"backend" to "onepassword",
|
||||
"display_name" to "1Password",
|
||||
),
|
||||
),
|
||||
)
|
||||
val reqEvent = EventParser.parse(reqResponse)
|
||||
assertTrue(reqEvent is WsEvent.VaultUnlockRequest)
|
||||
val unlock = reqEvent as WsEvent.VaultUnlockRequest
|
||||
assertEquals("req-unlock-1", unlock.requestId)
|
||||
assertEquals("sess-123", unlock.sessionId)
|
||||
assertEquals("onepassword", unlock.backend)
|
||||
assertEquals("1Password", unlock.displayName)
|
||||
|
||||
val expResponse =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "vault.unlock.expire",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to mapOf("request_id" to "req-unlock-1"),
|
||||
),
|
||||
)
|
||||
val expEvent = EventParser.parse(expResponse)
|
||||
assertTrue(expEvent is WsEvent.VaultUnlockExpire)
|
||||
assertEquals("req-unlock-1", (expEvent as WsEvent.VaultUnlockExpire).requestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseVaultSaveLoginRequest_andExpire() {
|
||||
val reqResponse =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "vault.save_login.request",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to
|
||||
mapOf(
|
||||
"request_id" to "req-save-1",
|
||||
"origin" to "https://github.com",
|
||||
"site" to "GitHub",
|
||||
),
|
||||
),
|
||||
)
|
||||
val reqEvent = EventParser.parse(reqResponse)
|
||||
assertTrue(reqEvent is WsEvent.VaultSaveLoginRequest)
|
||||
val save = reqEvent as WsEvent.VaultSaveLoginRequest
|
||||
assertEquals("req-save-1", save.requestId)
|
||||
assertEquals("https://github.com/login", "https://github.com/login")
|
||||
assertEquals("https://github.com", save.origin)
|
||||
assertEquals("GitHub", save.site)
|
||||
|
||||
val expResponse =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "vault.save_login.expire",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to mapOf("request_id" to "req-save-1"),
|
||||
),
|
||||
)
|
||||
val expEvent = EventParser.parse(expResponse)
|
||||
assertTrue(expEvent is WsEvent.VaultSaveLoginExpire)
|
||||
assertEquals("req-save-1", (expEvent as WsEvent.VaultSaveLoginExpire).requestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseVaultCodeRequest_andExpire() {
|
||||
val reqResponse =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "vault.code.request",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to
|
||||
mapOf(
|
||||
"request_id" to "req-code-1",
|
||||
"site" to "GitHub",
|
||||
"hint" to "SMS to +1234",
|
||||
),
|
||||
),
|
||||
)
|
||||
val reqEvent = EventParser.parse(reqResponse)
|
||||
assertTrue(reqEvent is WsEvent.VaultCodeRequest)
|
||||
val code = reqEvent as WsEvent.VaultCodeRequest
|
||||
assertEquals("req-code-1", code.requestId)
|
||||
assertEquals("GitHub", code.site)
|
||||
assertEquals("SMS to +1234", code.hint)
|
||||
|
||||
val expResponse =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "vault.code.expire",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to mapOf("request_id" to "req-code-1"),
|
||||
),
|
||||
)
|
||||
val expEvent = EventParser.parse(expResponse)
|
||||
assertTrue(expEvent is WsEvent.VaultCodeExpire)
|
||||
assertEquals("req-code-1", (expEvent as WsEvent.VaultCodeExpire).requestId)
|
||||
}
|
||||
|
||||
// ── TEST-07: Untested subtypes ─────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
+134
-52
@@ -7,11 +7,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -19,69 +21,149 @@ class ChatCredentialPromptsDelegateTest {
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val testScope = TestScope(testDispatcher)
|
||||
private val uiState = MutableStateFlow(ChatUiState(currentSessionId = "sess-1"))
|
||||
|
||||
private val sentMethods = mutableListOf<String>()
|
||||
private val sentParams = mutableListOf<Map<String, Any>>()
|
||||
private val trackedRequests = mutableListOf<Pair<String, String>>()
|
||||
|
||||
private val delegate =
|
||||
ChatCredentialPromptsDelegate(
|
||||
scope = testScope,
|
||||
ioDispatcher = testDispatcher,
|
||||
uiState = uiState,
|
||||
wsSend = { method, params, onSent ->
|
||||
sentMethods.add(method)
|
||||
sentParams.add(params)
|
||||
onSent?.invoke("req-${sentMethods.size}")
|
||||
},
|
||||
trackRequest = { id, method -> trackedRequests.add(id to method) },
|
||||
private lateinit var delegate: ChatCredentialPromptsDelegate
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
sentMethods.clear()
|
||||
sentParams.clear()
|
||||
uiState.value = ChatUiState(currentSessionId = "sess-1")
|
||||
delegate =
|
||||
ChatCredentialPromptsDelegate(
|
||||
scope = testScope,
|
||||
ioDispatcher = testDispatcher,
|
||||
uiState = uiState,
|
||||
wsSend = { method, params, _ ->
|
||||
sentMethods.add(method)
|
||||
sentParams.add(params)
|
||||
},
|
||||
trackRequest = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVaultUnlock_request_and_respond() {
|
||||
delegate.handleVaultUnlockRequest(
|
||||
WsEvent.VaultUnlockRequest(
|
||||
requestId = "req-1",
|
||||
sessionId = "sess-1",
|
||||
backend = "onepassword",
|
||||
displayName = "1Password",
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun handleSudoRequest_andRespond_clearsPromptAndSendsRpc() =
|
||||
testScope.runTest {
|
||||
delegate.handleSudoRequest(WsEvent.SudoRequest(requestId = "r-sudo", sessionId = "s-1"))
|
||||
assertEquals("r-sudo", uiState.value.sudoPrompt?.requestId)
|
||||
assertFalse(uiState.value.isAgentTyping)
|
||||
assertNotNull(uiState.value.vaultUnlockPrompt)
|
||||
assertEquals("req-1", uiState.value.vaultUnlockPrompt?.requestId)
|
||||
assertEquals("1Password", uiState.value.vaultUnlockPrompt?.displayName)
|
||||
|
||||
delegate.respondToSudo("secret-pass")
|
||||
assertNull(uiState.value.sudoPrompt)
|
||||
delegate.respondToVaultUnlock("master-pass")
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
advanceUntilIdle()
|
||||
assertEquals(listOf(WsMethods.SUDO_RESPOND), sentMethods)
|
||||
assertEquals("secret-pass", sentParams.first()["password"])
|
||||
assertEquals("r-sudo", sentParams.first()["request_id"])
|
||||
assertEquals("s-1", sentParams.first()["session_id"])
|
||||
}
|
||||
assertNull(uiState.value.vaultUnlockPrompt)
|
||||
assertEquals(listOf(WsMethods.VAULT_UNLOCK_RESPOND), sentMethods)
|
||||
assertEquals("master-pass", sentParams.first()["password"])
|
||||
assertEquals("req-1", sentParams.first()["request_id"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSecretRequest_andDismiss_sendsEmptyValue() =
|
||||
testScope.runTest {
|
||||
delegate.handleSecretRequest(
|
||||
WsEvent.SecretRequest(
|
||||
requestId = "r-sec",
|
||||
sessionId = "s-1",
|
||||
envVar = "OPENAI_API_KEY",
|
||||
prompt = "Enter key",
|
||||
),
|
||||
)
|
||||
assertEquals("OPENAI_API_KEY", uiState.value.secretPrompt?.envVar)
|
||||
fun testVaultUnlock_dismiss_sendsEmptyPassword() {
|
||||
delegate.handleVaultUnlockRequest(
|
||||
WsEvent.VaultUnlockRequest(
|
||||
requestId = "req-1",
|
||||
sessionId = "sess-1",
|
||||
),
|
||||
)
|
||||
|
||||
delegate.dismissSecret()
|
||||
assertNull(uiState.value.secretPrompt)
|
||||
delegate.dismissVaultUnlock()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
advanceUntilIdle()
|
||||
assertEquals(listOf(WsMethods.SECRET_RESPOND), sentMethods)
|
||||
assertEquals("", sentParams.first()["value"])
|
||||
assertEquals("r-sec", sentParams.first()["request_id"])
|
||||
}
|
||||
assertNull(uiState.value.vaultUnlockPrompt)
|
||||
assertEquals(listOf(WsMethods.VAULT_UNLOCK_RESPOND), sentMethods)
|
||||
assertEquals("", sentParams.first()["password"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expireHandlers_ignoreMismatchedRequestId() {
|
||||
delegate.handleSudoRequest(WsEvent.SudoRequest(requestId = "keep-me", sessionId = "s-1"))
|
||||
delegate.handleSudoExpire(WsEvent.SudoExpire(requestId = "other-req", sessionId = "s-1"))
|
||||
assertNotNull(uiState.value.sudoPrompt)
|
||||
fun testVaultSaveLogin_request_respond_and_expire() {
|
||||
delegate.handleVaultSaveLoginRequest(
|
||||
WsEvent.VaultSaveLoginRequest(
|
||||
requestId = "req-save",
|
||||
sessionId = "sess-1",
|
||||
origin = "https://github.com",
|
||||
site = "GitHub",
|
||||
),
|
||||
)
|
||||
|
||||
delegate.handleSudoExpire(WsEvent.SudoExpire(requestId = "keep-me", sessionId = "s-1"))
|
||||
assertNull(uiState.value.sudoPrompt)
|
||||
assertNotNull(uiState.value.vaultSaveLoginPrompt)
|
||||
assertEquals("GitHub", uiState.value.vaultSaveLoginPrompt?.site)
|
||||
|
||||
delegate.respondToVaultSaveLogin("user@example.com", "secret123")
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertNull(uiState.value.vaultSaveLoginPrompt)
|
||||
assertEquals(listOf(WsMethods.VAULT_SAVE_LOGIN_RESPOND), sentMethods)
|
||||
val loginJsonStr = sentParams.first()["login"] as String
|
||||
val json = Json.parseToJsonElement(loginJsonStr).jsonObject
|
||||
assertEquals("user@example.com", json["identifier"]?.jsonPrimitive?.content)
|
||||
assertEquals("secret123", json["password"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVaultSaveLogin_dismiss_sendsEmptyLogin() {
|
||||
delegate.handleVaultSaveLoginRequest(
|
||||
WsEvent.VaultSaveLoginRequest(
|
||||
requestId = "req-save",
|
||||
sessionId = "sess-1",
|
||||
),
|
||||
)
|
||||
|
||||
delegate.dismissVaultSaveLogin()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertNull(uiState.value.vaultSaveLoginPrompt)
|
||||
assertEquals(listOf(WsMethods.VAULT_SAVE_LOGIN_RESPOND), sentMethods)
|
||||
assertEquals("", sentParams.first()["login"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVaultCode_request_respond_and_dismiss() {
|
||||
delegate.handleVaultCodeRequest(
|
||||
WsEvent.VaultCodeRequest(
|
||||
requestId = "req-code",
|
||||
sessionId = "sess-1",
|
||||
site = "GitHub",
|
||||
hint = "SMS to phone",
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(uiState.value.vaultCodePrompt)
|
||||
assertEquals("GitHub", uiState.value.vaultCodePrompt?.site)
|
||||
|
||||
delegate.respondToVaultCode("654321")
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertNull(uiState.value.vaultCodePrompt)
|
||||
assertEquals(listOf(WsMethods.VAULT_CODE_RESPOND), sentMethods)
|
||||
assertEquals("654321", sentParams.first()["code"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVaultCode_dismiss_sendsEmptyCode() {
|
||||
delegate.handleVaultCodeRequest(
|
||||
WsEvent.VaultCodeRequest(
|
||||
requestId = "req-code",
|
||||
sessionId = "sess-1",
|
||||
),
|
||||
)
|
||||
|
||||
delegate.dismissVaultCode()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertNull(uiState.value.vaultCodePrompt)
|
||||
assertEquals(listOf(WsMethods.VAULT_CODE_RESPOND), sentMethods)
|
||||
assertEquals("", sentParams.first()["code"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.m57.hermescontrol.ui.settings
|
||||
|
||||
import com.m57.hermescontrol.data.model.VaultItem
|
||||
import com.m57.hermescontrol.data.model.VaultSource
|
||||
import com.m57.hermescontrol.data.ws.VaultRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SettingsVaultViewModelTest {
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private lateinit var mockRepo: VaultRepository
|
||||
|
||||
private val sampleSources =
|
||||
listOf(
|
||||
VaultSource(
|
||||
name = "local",
|
||||
displayName = "Hermes vault",
|
||||
enabled = true,
|
||||
unlocked = true,
|
||||
installed = true,
|
||||
),
|
||||
VaultSource(
|
||||
name = "onepassword",
|
||||
displayName = "1Password",
|
||||
enabled = true,
|
||||
needsUnlock = true,
|
||||
unlocked = false,
|
||||
installed = true,
|
||||
),
|
||||
)
|
||||
|
||||
private val sampleItems =
|
||||
listOf(
|
||||
VaultItem(id = "item-1", backend = "local", label = "GitHub", identifier = "user@example.com"),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
mockRepo = mockk()
|
||||
coEvery { mockRepo.getSources() } returns sampleSources
|
||||
coEvery { mockRepo.listItems() } returns sampleItems
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadVaultData loads sources and items successfully`() {
|
||||
val viewModel = SettingsVaultViewModel(vaultRepo = mockRepo, ioDispatcher = testDispatcher)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertFalse(viewModel.uiState.value.isLoading)
|
||||
assertEquals(2, viewModel.uiState.value.sources.size)
|
||||
assertEquals(1, viewModel.uiState.value.items.size)
|
||||
assertNull(viewModel.uiState.value.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toggleSource updates enabled state and reloads sources`() {
|
||||
coEvery { mockRepo.setSourceEnabled("onepassword", false) } returns false
|
||||
coEvery { mockRepo.getSources() } returns
|
||||
listOf(
|
||||
sampleSources[0],
|
||||
sampleSources[1].copy(enabled = false),
|
||||
)
|
||||
|
||||
val viewModel = SettingsVaultViewModel(vaultRepo = mockRepo, ioDispatcher = testDispatcher)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.toggleSource(sampleSources[1], false)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { mockRepo.setSourceEnabled("onepassword", false) }
|
||||
assertFalse(
|
||||
viewModel.uiState.value.sources[1]
|
||||
.enabled,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlockSource calls repo and updates state`() {
|
||||
coEvery { mockRepo.unlockSource("onepassword", "mypassword") } returns true
|
||||
coEvery { mockRepo.getSources() } returns
|
||||
listOf(
|
||||
sampleSources[0],
|
||||
sampleSources[1].copy(unlocked = true),
|
||||
)
|
||||
|
||||
val viewModel = SettingsVaultViewModel(vaultRepo = mockRepo, ioDispatcher = testDispatcher)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.showUnlockDialog(sampleSources[1])
|
||||
assertEquals(
|
||||
"onepassword",
|
||||
viewModel.uiState.value.unlockDialogSource
|
||||
?.name,
|
||||
)
|
||||
|
||||
viewModel.unlockSource("onepassword", "mypassword")
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { mockRepo.unlockSource("onepassword", "mypassword") }
|
||||
assertNull(viewModel.uiState.value.unlockDialogSource)
|
||||
assertTrue(
|
||||
viewModel.uiState.value.sources[1]
|
||||
.unlocked,
|
||||
)
|
||||
assertEquals("Unlocked onepassword", viewModel.uiState.value.toastMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lockSource locks all and notifies`() {
|
||||
coEvery { mockRepo.lockSource(null) } returns true
|
||||
coEvery { mockRepo.getSources() } returns sampleSources
|
||||
|
||||
val viewModel = SettingsVaultViewModel(vaultRepo = mockRepo, ioDispatcher = testDispatcher)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.lockSource(null)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { mockRepo.lockSource(null) }
|
||||
assertEquals("All password managers locked", viewModel.uiState.value.toastMessage)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user