Skip to content

Commit

Permalink
Make SSH passwords optional when creating profile
Browse files Browse the repository at this point in the history
This affects both login password & private key password. These will be
queried using LoginFragment if not available in profile.

LoginInfo now serves as a generic wrapper for credentials, instead of
being tied to VNC credentials.

Re: #131
  • Loading branch information
gujjwal00 committed Apr 8, 2023
1 parent 8408af8 commit 42b8fb8
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 106 deletions.
24 changes: 23 additions & 1 deletion app/src/androidTest/java/com/gaurav/avnc/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import android.view.View
import android.widget.ProgressBar
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.platform.app.InstrumentationRegistry
import junit.framework.AssertionFailedError
Expand All @@ -49,7 +51,8 @@ fun ViewInteraction.checkWillBeDisplayed() = checkWithTimeout(matches(isDisplaye
fun ViewInteraction.checkIsNotDisplayed() = check(matches(not(isDisplayed())))!!
fun ViewInteraction.doClick() = perform(click())!!
fun ViewInteraction.doLongClick() = perform(longClick())!!
fun ViewInteraction.doTypeText(text: String) = perform(typeText(text))!!
fun ViewInteraction.doTypeText(text: String) = perform(typeText(text)).perform(closeSoftKeyboard())!!
fun ViewInteraction.inDialog() = inRoot(RootMatchers.isDialog())!!


/**
Expand All @@ -75,6 +78,25 @@ fun ViewInteraction.checkWithTimeout(assertion: ViewAssertion, timeout: Int = 50
throw Exception("Assertion did not become valid within timeout", t)
}

/**
* Runs given [block] in context of scenario's activity.
* It simplifies the pattern of getting some value using activity.
*/
fun <A : Activity, R> ActivityScenario<A>.withActivity(block: A.() -> R): R {
var r: R? = null
onActivity { r = it.block() }
return r!!
}

/**
* Runs given block synchronously on main thread.
*/
fun <R> runOnMainSync(block: () -> R): R {
var r: R? = null
instrumentation.runOnMainSync { r = block() }
return r!!
}

fun getClipboardText(): String? {
var text: String? = null
instrumentation.runOnMainSync {
Expand Down
161 changes: 161 additions & 0 deletions app/src/androidTest/java/com/gaurav/avnc/ui/vnc/LoginFragmentTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2023 Gaurav Ujjwal.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* See COPYING.txt for more details.
*/

/*
* Copyright (c) 2023 Gaurav Ujjwal.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* See COPYING.txt for more details.
*/

package com.gaurav.avnc.ui.vnc

import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onIdle
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.gaurav.avnc.*
import com.gaurav.avnc.model.LoginInfo
import com.gaurav.avnc.model.ServerProfile
import com.gaurav.avnc.model.db.MainDb
import com.gaurav.avnc.viewmodel.VncViewModel
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import java.io.Closeable
import kotlin.concurrent.thread

private const val SAMPLE_USERNAME = "Chandler"
private const val SAMPLE_PASSWORD = "Bing"

class LoginFragmentTest {

private class Scenario(private val profileTemplate: ServerProfile? = null) : Closeable {
private val dao = MainDb.getInstance(targetContext).serverProfileDao
private val profile = setupProfile()
private var loginInfo = LoginInfo()
private val activityScenario = ActivityScenario.launch<VncActivity>(createVncIntent(targetContext, profile))
private val viewModel = activityScenario.withActivity { viewModel }
private var loginInfoThread: Thread? = null

private fun setupProfile() = runBlocking {
dao.deleteAll()
(profileTemplate?.copy() ?: ServerProfile()).apply { ID = dao.insert(this) }
}

fun triggerLoginInfoRequest(type: LoginInfo.Type) {
loginInfoThread = thread { loginInfo = viewModel.getLoginInfo(type) }
}

fun waitForLoginInfo(): LoginInfo {
loginInfoThread!!.join(5000)
return loginInfo
}

fun triggerLoginSave(): ServerProfile {
viewModel.state.postValue(VncViewModel.State.Connected)
onIdle()
return viewModel.profile
}

override fun close() {
activityScenario.close()
}
}

private fun passwordLogin(type: LoginInfo.Type) = Scenario().use { scenario ->
scenario.triggerLoginInfoRequest(type)
onView(withId(R.id.password)).inDialog().checkWillBeDisplayed().doTypeText(SAMPLE_PASSWORD)
onView(withId(R.id.username)).inDialog().checkIsNotDisplayed()
onView(withText(android.R.string.ok)).inDialog().checkIsDisplayed().doClick()
assertEquals(SAMPLE_PASSWORD, scenario.waitForLoginInfo().password)
}

@Test
fun vncPasswordLogin() = passwordLogin(LoginInfo.Type.VNC_PASSWORD)

@Test
fun sshPasswordLogin() = passwordLogin(LoginInfo.Type.SSH_PASSWORD)

@Test
fun sshKeyPasswordLogin() = passwordLogin(LoginInfo.Type.SSH_KEY_PASSWORD)

@Test
fun vncCredentialLoginWithRememberChecked() = Scenario().use { scenario ->
scenario.triggerLoginInfoRequest(LoginInfo.Type.VNC_CREDENTIAL)
onView(withId(R.id.username)).inDialog().checkWillBeDisplayed().doTypeText(SAMPLE_USERNAME)
onView(withId(R.id.password)).inDialog().checkWillBeDisplayed().doTypeText(SAMPLE_PASSWORD)
onView(withId(R.id.remember)).inDialog().checkIsDisplayed().doClick()
onView(withText(android.R.string.ok)).inDialog().checkIsDisplayed().doClick()

val l = scenario.waitForLoginInfo()
val p = scenario.triggerLoginSave()
assertEquals(SAMPLE_USERNAME, l.username)
assertEquals(SAMPLE_PASSWORD, l.password)
assertEquals(SAMPLE_USERNAME, p.username)
assertEquals(SAMPLE_PASSWORD, p.password)
}

@Test
fun vncCredentialLoginWhenPasswordIsAvailable() = Scenario(ServerProfile(password = SAMPLE_PASSWORD)).use { scenario ->
scenario.triggerLoginInfoRequest(LoginInfo.Type.VNC_CREDENTIAL)
onView(withId(R.id.username)).inDialog().checkWillBeDisplayed().doTypeText(SAMPLE_USERNAME)
onView(withId(R.id.password)).inDialog().checkIsNotDisplayed()
onView(withText(android.R.string.ok)).inDialog().checkIsDisplayed().doClick()

val l = scenario.waitForLoginInfo()
assertEquals(SAMPLE_USERNAME, l.username)
assertEquals(SAMPLE_PASSWORD, l.password)
}

@Test
fun sshPasswordLoginWithRememberChecked() = Scenario().use { scenario ->
scenario.triggerLoginInfoRequest(LoginInfo.Type.SSH_PASSWORD)
onView(withId(R.id.password)).inDialog().checkWillBeDisplayed().doTypeText(SAMPLE_PASSWORD)
onView(withId(R.id.remember)).inDialog().checkIsDisplayed().doClick()
onView(withText(android.R.string.ok)).inDialog().checkIsDisplayed().doClick()

val l = scenario.waitForLoginInfo()
val p = scenario.triggerLoginSave()
assertEquals(SAMPLE_PASSWORD, l.password)
assertEquals(SAMPLE_PASSWORD, p.sshPassword)
}

@Test
fun sshKeyPasswordLoginWithRememberChecked() = Scenario().use { scenario ->
scenario.triggerLoginInfoRequest(LoginInfo.Type.SSH_KEY_PASSWORD)
onView(withId(R.id.password)).inDialog().checkWillBeDisplayed().doTypeText(SAMPLE_PASSWORD)
onView(withId(R.id.remember)).inDialog().checkIsDisplayed().doClick()
onView(withText(android.R.string.ok)).inDialog().checkIsDisplayed().doClick()

val l = scenario.waitForLoginInfo()
val p = scenario.triggerLoginSave()
assertEquals(SAMPLE_PASSWORD, l.password)
assertEquals(SAMPLE_PASSWORD, p.sshPrivateKeyPassword)
}

/**
* If login information is already available in profile,
* [VncViewModel] should provide it without triggering login dialog.
*/
@Test(timeout = 5000)
fun savedLoginTest() {
val profile = ServerProfile(username = "AB", password = "BC", sshPassword = "CD", sshPrivateKeyPassword = "DE")
val viewModel = runOnMainSync { VncViewModel(profile, ApplicationProvider.getApplicationContext()) }

assertEquals("AB", viewModel.getLoginInfo(LoginInfo.Type.VNC_CREDENTIAL).username)
assertEquals("BC", viewModel.getLoginInfo(LoginInfo.Type.VNC_CREDENTIAL).password)
assertEquals("BC", viewModel.getLoginInfo(LoginInfo.Type.VNC_PASSWORD).password)

assertEquals("CD", viewModel.getLoginInfo(LoginInfo.Type.SSH_PASSWORD).password)
assertEquals("DE", viewModel.getLoginInfo(LoginInfo.Type.SSH_KEY_PASSWORD).password)
}
}
20 changes: 14 additions & 6 deletions app/src/main/java/com/gaurav/avnc/model/LoginInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@
package com.gaurav.avnc.model

/**
* Simple model used by login auto-completion.
* Generic wrapper for login information.
* This can be used to hold different [Type]s of credentials.
*/
data class LoginInfo(
val name: String, //Profile name
val host: String,
val username: String,
val password: String,
)
var name: String = "", // Profile name
var host: String = "",
var username: String = "",
var password: String = "",
) {
enum class Type {
VNC_PASSWORD,
VNC_CREDENTIAL, // Username & Password
SSH_PASSWORD,
SSH_KEY_PASSWORD
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,6 @@ class ProfileEditorFragment : DialogFragment() {
result = result and
validateNotEmpty(binding.sshHost) and
validateNotEmpty(binding.sshUsername) and
validateNotEmpty(binding.sshPassword, binding.sshAuthTypePassword.isChecked) and
validatePrivateKey()
}

Expand All @@ -286,11 +285,6 @@ class ProfileEditorFragment : DialogFragment() {
binding.keyImportBtn.error = "Required"
return false
}

if (binding.isPrivateKeyEncrypted && binding.sshKeyPassword.length() == 0) {
binding.sshKeyPassword.error = "Password is required for encrypted Private Key"
return false
}
}
return true
}
Expand Down
Loading

0 comments on commit 42b8fb8

Please sign in to comment.