Trying AI generated UI tests

This commit is contained in:
Sylvain Berfini 2026-03-24 16:34:49 +01:00
parent 721e379b50
commit 0aca829ba7
7 changed files with 197 additions and 9 deletions

View file

@ -267,6 +267,12 @@ dependencies {
implementation(libs.openid.appauth)
implementation(libs.linphone)
testImplementation(libs.junit)
testImplementation(libs.androidx.test.core)
testImplementation(libs.androidx.arch.core.testing)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {

View file

@ -55,6 +55,7 @@
android:theme="@style/Theme.Linphone"
android:appCategory="social"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="35">
<!-- Required for chat message & call notifications to be displayed in Android auto -->

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.sso.utils
import android.net.Uri
import net.openid.appauth.connectivity.ConnectionBuilder
import java.net.HttpURLConnection
import java.net.URL
import java.security.cert.X509Certificate
import javax.net.ssl.*
class AppAuthConnectionBuilder(private val trustAll: Boolean) : ConnectionBuilder {
override fun openConnection(uri: Uri): HttpURLConnection {
val conn = URL(uri.toString()).openConnection() as HttpURLConnection
if (trustAll && conn is HttpsURLConnection) {
conn.sslSocketFactory = TRUSTING_CONTEXT.socketFactory
conn.hostnameVerifier = HostnameVerifier { _, _ -> true }
}
return conn
}
companion object {
private val TRUSTING_CONTEXT: SSLContext by lazy {
val context = SSLContext.getInstance("TLS")
context.init(null, arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}), java.security.SecureRandom())
context
}
}
}

View file

@ -21,10 +21,12 @@ package org.linphone.ui.main.sso.viewmodel
import android.content.Intent
import androidx.annotation.UiThread
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import java.io.File
import kotlinx.coroutines.launch
import net.openid.appauth.AppAuthConfiguration
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
@ -39,10 +41,10 @@ import org.linphone.R
import org.linphone.core.Factory
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.ui.main.sso.utils.AppAuthConnectionBuilder
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
import org.linphone.utils.TimestampUtils
import androidx.core.net.toUri
class SingleSignOnViewModel
@UiThread
@ -127,9 +129,12 @@ class SingleSignOnViewModel
@UiThread
private fun singleSignOn() {
Log.i("$TAG Fetch from issuer [$singleSignOnUrl]")
AuthorizationServiceConfiguration.fetchFromIssuer(
singleSignOnUrl.toUri(),
AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex ->
val connectionBuilder = AppAuthConnectionBuilder(true)
val callback = object : AuthorizationServiceConfiguration.RetrieveConfigurationCallback {
override fun onFetchConfigurationCompleted(
serviceConfiguration: AuthorizationServiceConfiguration?,
ex: AuthorizationException?
) {
if (ex != null) {
Log.e(
"$TAG Failed to fetch configuration from issuer [$singleSignOnUrl]: ${ex.errorDescription}"
@ -137,13 +142,13 @@ class SingleSignOnViewModel
onErrorEvent.postValue(
Event("Failed to fetch configuration from issuer $singleSignOnUrl")
)
return@RetrieveConfigurationCallback
return
}
if (serviceConfiguration == null) {
Log.e("$TAG Service configuration is null!")
onErrorEvent.postValue(Event("Service configuration is null"))
return@RetrieveConfigurationCallback
return
}
if (!::authState.isInitialized) {
@ -169,10 +174,19 @@ class SingleSignOnViewModel
}
val authRequest = authRequestBuilder.build()
authService = AuthorizationService(coreContext.context)
val authConfig = AppAuthConfiguration.Builder()
.setConnectionBuilder(connectionBuilder)
.build()
authService = AuthorizationService(coreContext.context, authConfig)
val authIntent = authService.getAuthorizationRequestIntent(authRequest)
startAuthIntentEvent.postValue(Event(authIntent))
}
}
AuthorizationServiceConfiguration.fetchFromIssuer(
singleSignOnUrl.toUri(),
callback,
connectionBuilder
)
}
@ -180,7 +194,10 @@ class SingleSignOnViewModel
private fun performRefreshToken() {
if (::authState.isInitialized) {
if (!::authService.isInitialized) {
authService = AuthorizationService(coreContext.context)
val authConfig = AppAuthConfiguration.Builder()
.setConnectionBuilder(AppAuthConnectionBuilder(true))
.build()
authService = AuthorizationService(coreContext.context, authConfig)
}
val authStateJsonFile = File(corePreferences.ssoCacheFile)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.assistant.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.linphone.LinphoneApplication
import org.linphone.core.CoreContext
import org.linphone.core.CorePreferences
class AccountLoginViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: AccountLoginViewModel
private val coreContext: CoreContext = mockk(relaxed = true)
private val corePreferences: CorePreferences = mockk(relaxed = true)
@Before
fun setUp() {
mockkObject(LinphoneApplication.Companion)
every { LinphoneApplication.coreContext } returns coreContext
every { LinphoneApplication.corePreferences } returns corePreferences
// Mock postOnCoreThread to execute the lambda immediately
every { coreContext.postOnCoreThread(any()) } answers {
val lambda = invocation.args[0] as (org.linphone.core.Core) -> Unit
lambda(mockk(relaxed = true))
}
viewModel = AccountLoginViewModel()
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun testLoginButtonEnabledOnlyWhenBothFieldsAreFilled() {
// MediatorLiveData needs an observer to be active and trigger its logic
viewModel.loginEnabled.observeForever { }
// Initial state: both empty
viewModel.sipIdentity.value = ""
viewModel.password.value = ""
assertEquals("Login button should be disabled when both fields are empty", false, viewModel.loginEnabled.value)
// Only username filled
viewModel.sipIdentity.value = "testuser"
viewModel.password.value = ""
assertEquals("Login button should be disabled when password is empty", false, viewModel.loginEnabled.value)
// Only password filled
viewModel.sipIdentity.value = ""
viewModel.password.value = "testpassword"
assertEquals("Login button should be disabled when username is empty", false, viewModel.loginEnabled.value)
// Both filled
viewModel.sipIdentity.value = "testuser"
viewModel.password.value = "testpassword"
assertEquals("Login button should be enabled when both fields are filled", true, viewModel.loginEnabled.value)
// Username with only spaces
viewModel.sipIdentity.value = " "
viewModel.password.value = "testpassword"
assertEquals("Login button should be disabled when username is only whitespace", false, viewModel.loginEnabled.value)
}
}

View file

@ -32,6 +32,11 @@ dotsIndicator = "5.1.0"
photoview = "2.3.0"
openidAppauth = "0.11.1"
linphone = "5.5.+"
junit = "4.13.2"
androidxTestCore = "1.6.1"
androidxArchCore = "2.2.0"
kotlinxCoroutinesTest = "1.10.1"
mockk = "1.13.16"
[libraries]
androidx-annotations = { group = "androidx.annotation", name = "annotation", version.ref = "annotations" }
@ -69,6 +74,12 @@ openid-appauth = { group = "net.openid", name = "appauth", version.ref = "openid
linphone = { group = "org.linphone", name = "linphone-sdk-android", version.ref = "linphone" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" }
androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidxArchCore" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
@ -76,4 +87,4 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
navigation = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" }
crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" }
googleGmsServices = { id = "com.google.gms.google-services", version.ref = "gmsGoogleServices" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }