Using custom credential provider in Keycloak - keycloak

I have created a custom credential provider PinCredentialProvider with a corresponding PinCredentialProviderFactory for Keycloak.
class PinCredentialProviderFactory: CredentialProviderFactory<PinCredentialProvider> {
companion object {
final val PROVIDER_ID = "pin"
}
override fun create(session: KeycloakSession): CredentialProvider<*> = PinCredentialProvider(session)
override fun getId(): String = PROVIDER_ID
}
class PinCredentialProvider(
private val session: KeycloakSession
): CredentialProvider<PinCredentialModel>, CredentialInputValidator {
override fun getType(): String = PinCredentialModel.TYPE
override fun createCredential(realm: RealmModel, user: UserModel, credentialModel: PinCredentialModel): CredentialModel {
if(credentialModel.createdDate !is Long) {
credentialModel.createdDate = Time.currentTimeMillis()
}
return getCredentialStore().createCredential(realm, user, credentialModel)
}
override fun deleteCredential(realm: RealmModel, user: UserModel, credentialId: String): Boolean =
getCredentialStore().removeStoredCredential(realm, user, credentialId)
override fun getCredentialFromModel(credentialModel: CredentialModel): PinCredentialModel =
PinCredentialModel.createFromCredentialModel(credentialModel)
override fun getCredentialTypeMetadata(context: CredentialTypeMetadataContext): CredentialTypeMetadata =
CredentialTypeMetadata.builder()
.type(type)
.category(CredentialTypeMetadata.Category.BASIC_AUTHENTICATION) // TODO is this correct?
.displayName(PinCredentialProviderFactory.PROVIDER_ID)
.helpText("pin credential provider")
//.createAction()
//.removable(false)
.build(session)
override fun supportsCredentialType(credentialType: String): Boolean = type == credentialType
override fun isConfiguredFor(realm: RealmModel, user: UserModel, credentialType: String): Boolean {
if(supportsCredentialType(credentialType).not()) return false
return getCredentialStore().getStoredCredentialsByTypeStream(realm, user, credentialType).count() > 0
}
override fun isValid(realm: RealmModel, user: UserModel, input: CredentialInput?): Boolean {
return true
}
private fun getCredentialStore(): UserCredentialManager = session.userCredentialManager()
}
It works fine and the custom provider is displayed on the server info -> providers page of Keycloak.
The question is now, how can I use the credential type for users:

Related

for plugin in_app_update, Type mismatch: inferred type is String? but String was expected

This is in my flutter project which was working good yesterday before flutter upgrade, and after that I updated my kotlin version too.
I am using in_app_update plugin version 2.0.0
package de.ffuf.in_app_update
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.os.Bundle
import androidx.annotation.NonNull
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.InstallErrorCode
import com.google.android.play.core.install.model.UpdateAvailability
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
interface ActivityProvider {
fun addActivityResultListener(callback: PluginRegistry.ActivityResultListener)
fun activity(): Activity?
}
class InAppUpdatePlugin : FlutterPlugin, MethodCallHandler,
PluginRegistry.ActivityResultListener, Application.ActivityLifecycleCallbacks, ActivityAware {
companion object {
private const val REQUEST_CODE_START_UPDATE = 1276
}
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(#NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(
flutterPluginBinding.binaryMessenger,
"in_app_update"
)
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(#NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
private var activityProvider: ActivityProvider? = null
private var updateResult: Result? = null
private var appUpdateType: Int? = null
private var appUpdateInfo: AppUpdateInfo? = null
private var appUpdateManager: AppUpdateManager? = null
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"checkForUpdate" -> checkForUpdate(result)
"performImmediateUpdate" -> performImmediateUpdate(result)
"startFlexibleUpdate" -> startFlexibleUpdate(result)
"completeFlexibleUpdate" -> completeFlexibleUpdate(result)
else -> result.notImplemented()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == REQUEST_CODE_START_UPDATE && appUpdateType == AppUpdateType.IMMEDIATE) {
if (resultCode != RESULT_OK) {
updateResult?.error("Update failed", resultCode.toString(), null)
} else {
updateResult?.success(null)
}
updateResult = null
return true
}
return false
}
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
activityProvider = object : ActivityProvider {
override fun addActivityResultListener(callback: PluginRegistry.ActivityResultListener) {
activityPluginBinding.addActivityResultListener(callback)
}
override fun activity(): Activity? {
return activityPluginBinding.activity
}
}
}
override fun onDetachedFromActivityForConfigChanges() {
activityProvider = null
}
override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) {
activityProvider = object : ActivityProvider {
override fun addActivityResultListener(callback: PluginRegistry.ActivityResultListener) {
activityPluginBinding.addActivityResultListener(callback)
}
override fun activity(): Activity? {
return activityPluginBinding.activity
}
}
}
override fun onDetachedFromActivity() {
activityProvider = null
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityDestroyed(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {
appUpdateManager
?.appUpdateInfo
?.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability()
== UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
&& appUpdateType == AppUpdateType.IMMEDIATE
) {
appUpdateManager?.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
activity,
REQUEST_CODE_START_UPDATE
)
}
}
}
private fun performImmediateUpdate(result: Result) = checkAppState(result) {
appUpdateType = AppUpdateType.IMMEDIATE
updateResult = result
appUpdateManager?.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
activityProvider?.activity(),
REQUEST_CODE_START_UPDATE
)
}
private fun checkAppState(result: Result, block: () -> Unit) {
requireNotNull(appUpdateInfo) {
result.error("Call checkForUpdate first!", null, null)
}
requireNotNull(activityProvider?.activity()) {
result.error("in_app_update requires a foreground activity", null, null)
}
requireNotNull(appUpdateManager) {
result.error("Call checkForUpdate first!", null, null)
}
block()
}
private fun startFlexibleUpdate(result: Result) = checkAppState(result) {
appUpdateType = AppUpdateType.FLEXIBLE
updateResult = result
appUpdateManager?.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
activityProvider?.activity(),
REQUEST_CODE_START_UPDATE
)
appUpdateManager?.registerListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
updateResult?.success(null)
updateResult = null
} else if (state.installErrorCode() != InstallErrorCode.NO_ERROR) {
updateResult?.error(
"Error during installation",
state.installErrorCode().toString(),
null
)
updateResult = null
}
}
}
private fun completeFlexibleUpdate(result: Result) = checkAppState(result) {
appUpdateManager?.completeUpdate()
}
private fun checkForUpdate(result: Result) {
requireNotNull(activityProvider?.activity()) {
result.error("in_app_update requires a foreground activity", null, null)
}
activityProvider?.addActivityResultListener(this)
activityProvider?.activity()?.application?.registerActivityLifecycleCallbacks(this)
appUpdateManager = AppUpdateManagerFactory.create(activityProvider?.activity())
// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager!!.appUpdateInfo
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener { info ->
appUpdateInfo = info
result.success(
mapOf(
"updateAvailability" to info.updateAvailability(),
"immediateAllowed" to info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE),
"flexibleAllowed" to info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE),
"availableVersionCode" to info.availableVersionCode(), //Nullable according to docs
"installStatus" to info.installStatus(),
"packageName" to info.packageName(),
"clientVersionStalenessDays" to info.clientVersionStalenessDays(), //Nullable according to docs
"updatePriority" to info.updatePriority()
)
)
}
appUpdateInfoTask.addOnFailureListener {
result.error(it.message, null, null) ?: ""
}
}
}
I think I need to update the plugin but this plugin depends on another and the cycle goes on. Is there any other way to solve it.
I don't know about kotlin and its null safety.

How I can update my adapter for RecyclerView after change my LiveData?

I created a fragment that in the onActivityCreated method fetches Firebase data by limiting the query to a calendar date. Then I place Observers on my LiveData that are inside my ViewModel and that will deliver the list to my Adapter.
If I add, remove or update items in the same list, the changes are sent to firebase and the adapter reflects them on the screen. It works ok.
But, I am trying to develop a filter button, which will basically change the deadline date for the Firebase query. When I select a particular filter, the viewModel needs to retrieve the data from Firebase limited to the filter date. This generates a new list, having a different size from the previous one.
However, when the query occurs, the Adapter's getItemCount() method stores the size of the last list. This fact confuses the Adapter and the functions notifyItemInserted and notifyItemRemoved end up making confusing animations on the screen after changing the filter. I dont know whats is wrong.
How can I correctly observes LiveData and tell the adapter? Am I making a mistake in the MVVM architecture or forgetting some function?
My Fragment:
class HistoryFragment : Fragment(), OnItemMenuRecyclerViewClickListener {
private lateinit var mSecurityPreferences: SecurityPreferences
private lateinit var viewModel: BalancesViewModel
private lateinit var adapter: BalancesAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setHasOptionsMenu(true)
viewModel = ViewModelProvider(this).get(BalancesViewModel::class.java)
adapter = BalancesAdapter(requireContext())
mSecurityPreferences = SecurityPreferences(requireContext())
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupFilter()
//Setup adapter
adapter.listenerMenu = this
recycler_view_history.adapter = adapter
//Fetch data based in filter by date
viewModel.fetchBalances(mSecurityPreferences.getStoredLong(FILTER_DATE))
// Put logic to listen RealTimeUpdates
viewModel.getRealTimeUpdates(mSecurityPreferences.getStoredLong(FILTER_DATE))
viewModel.balances.observe(viewLifecycleOwner, Observer {
adapter.setBalances(it)
})
viewModel.balance.observe(viewLifecycleOwner, Observer {
adapter.addBalance(it)
})
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.history_menu_filter, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.item_menu_filter_this_month -> {
updateFilter(THIS_MONTH)
}
R.id.item_menu_filter_two_months -> {
updateFilter(TWO_MONTHS)
}
R.id.item_menu_filter_last_six_months -> {
updateFilter(LAST_SIX_MONTHS)
}
R.id.item_menu_filter_all -> {
updateFilter(ALL_MONTHS)
}
}
return super.onOptionsItemSelected(item)
}
private fun setupFilter() {
var filterOption = mSecurityPreferences.getStoredLong(FILTER_DATE)
if (filterOption == 0L){
filterOption = HandleDate.getLongToFilter(LAST_SIX_MONTHS)
mSecurityPreferences.storeLong(FILTER_DATE, filterOption)
}
}
private fun updateFilter(filterOption: Int){
val newFilterOption = HandleDate.getLongToFilter(filterOption)
mSecurityPreferences.storeLong(FILTER_DATE, newFilterOption)
updateUI()
}
private fun updateUI(){
viewModel.fetchBalances(mSecurityPreferences.getStoredLong(FILTER_DATE))
viewModel.getRealTimeUpdates(mSecurityPreferences.getStoredLong(FILTER_DATE))
}
}
My ViewModel:
class BalancesViewModel : ViewModel() {
private val userReference = FirebaseAuth.getInstance().currentUser!!.uid
private val dbUserReference = FirebaseDatabase.getInstance().getReference(userReference)
private val _balances = MutableLiveData<List<Balance>>()
val balances: LiveData<List<Balance>>
get() = _balances
private val _balance = MutableLiveData<Balance>()
val balance: LiveData<Balance>
get() = _balance
private val _result = MutableLiveData<Exception?>()
val result: LiveData<Exception?>
get() = _result
fun addBalance(balance: Balance) {
balance.id = dbUserReference.push().key
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(balance)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
private val childEventListener = object : ChildEventListener {
override fun onCancelled(error: DatabaseError) {
}
override fun onChildMoved(snapshot: DataSnapshot, p1: String?) {
}
override fun onChildChanged(snapshot: DataSnapshot, p1: String?) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
_balance.value = balance
}
override fun onChildAdded(snapshot: DataSnapshot, p1: String?) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
_balance.value = balance
}
override fun onChildRemoved(snapshot: DataSnapshot) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
balance?.isDeleted = true
_balance.value = balance
}
}
fun getRealTimeUpdates(longLimitDate: Long) {
dbUserReference.child(NODE_BALANCES).orderByChild(COLUMN_DATE_MILLI)
.startAt(longLimitDate.toDouble()).addChildEventListener(childEventListener)
}
fun fetchBalances(longLimitDate: Long) {
dbUserReference.child(NODE_BALANCES).orderByChild(COLUMN_DATE_MILLI)
.startAt(longLimitDate.toDouble())
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
val listBalances = mutableListOf<Balance>()
for (balanceSnapshot in (snapshot.children)) {
val balance = balanceSnapshot.getValue(Balance::class.java)
balance?.id = balanceSnapshot.key
balance?.let { listBalances.add(it) }
}
listBalances.sortByDescending { it.dateMilli }
_balances.value = listBalances
}
}
})
}
fun updateBalance(balance: Balance) {
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(balance)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
fun deleteBalance(balance: Balance) {
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(null)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
My Adapter:
class BalancesAdapter(private val context: Context) :
RecyclerView.Adapter<BalancesAdapter.BalanceViewModel>() {
private var balances = mutableListOf<Balance>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
BalanceViewModel(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_recyclerview_balance, parent, false)
)
override fun getItemCount() = balances.size
override fun onBindViewHolder(holder: BalanceViewModel, position: Int) {
holder.view.text_view_value_balance_item.text = balances[position].value
holder.view.text_view_date_item.text = balances[position].date
}
fun setBalances(balances: List<Balance>) {
this.balances = balances as MutableList<Balance>
notifyDataSetChanged()
}
fun addBalance(balance: Balance) {
val index = balances.indexOf(balance)
if (!balances.contains(balance)) {
balances.add(balance)
notifyItemInserted(index)
} else {
if (balance.isDeleted) {
balances.removeAt(index)
notifyItemRemoved(index)
} else {
balances[index] = balance
}
}
notifyItemRangeChanged(index, itemCount)
}
class BalanceViewModel(val view: View) : RecyclerView.ViewHolder(view)
}
Tnks for your attention.
Okay, it's been 4 days since I asked this question and after feeling a little frustrated with the project I come back here on StackOverFlow to post my own answer.
The problematic issue within the code I showed is in my Adapter's addBalance method.
When I created the Balance data model, I set the isDeleted attribute to identify that it was deleted. Upon entering Firebase it receives a NULL value and therefore it ceases to exist.
Then, as I have two listeners (one defined in the addListenerForSingleValueEvent method and the other defined in the addChildEventListener method), one ends up triggering the other when there is a change in the Firebase data, but I don't want to go into detail on that issue. The fact is that I checked that my addBalance method was being called after I deleted an object, causing that object to be inserted back into the Adapter's data list, even before the removal operation ended in Firebase.
So I changed the logic of my method to make sure that my object was deleted and only included it in my Adapter list after checking the isDeleted attribute.
fun dealWithBalance(balance: Balance){
val index = balances.indexOf(balance)
if(balance.isDeleted && balances.contains(balance)){
balances.removeAt(index)
notifyItemRemoved(index)
} else if(!balance.isDeleted && !balances.contains(balance)){
balances.add(balance)
} else if(index >= 0){
balances[index] = balance
notifyItemChanged(index)
}
}
I renamed addBalance to dealWithBalance...

Save in ReactiveCrudRepository not inserting or updating records

As stated in the title I'm not been able to insert or update records in my Postgres database.
I just started working with spring and kotlin so there might be some pretty basic configuration that it's missing. Thanks in advance
Here is my code base
UserRepository
#Repository
interface UserRepository : ReactiveCrudRepository<User, Int> {}
User model
#Table("user_app")
data class User (
#Id
#Column("id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
val id : Int? = null,
#Column("username")
val username : String?,
#Column("email")
val email : String?,
#Column("name")
val name : String?,
#Column("password")
val password : String?
)
UserController
#Component
class UserController{
#Autowired
private lateinit var userRepository : UserRepository
fun getAllUsers() : Flux<User> = userRepository.findAll()
fun getUserById( userId: Int) : Mono<User> = userRepository.findById(userId)
fun createUser(user: User): Mono<User> = userRepository.save(user)
fun updateUser(user: User) : Mono<User> = userRepository.save(user)
}
UserConfiguration
#Configuration
class UserConfiguration {
#FlowPreview
#Bean
fun userRouter(controller: UserController): RouterFunction<ServerResponse> = router{
("/users").nest{
GET("") { _ ->
ServerResponse.ok().body(controller.getAllUsers())
}
GET("/{id}") { req ->
ServerResponse.ok().body(controller.getUserById(req.pathVariable("id").toInt()))
}
accept(MediaType.APPLICATION_JSON)
POST("") {req ->
ServerResponse.ok().body(req.bodyToMono(User::class.java).doOnNext { user ->
controller.createUser(user) })
}
accept(MediaType.APPLICATION_JSON)
PUT("") {req ->
ServerResponse.ok().body(req.bodyToMono(User::class.java).doOnNext { user ->
run {
controller.updateUser(user)
}
})
}
}
}
}
R2dbcConfiguration
#Configuration
#EnableR2dbcRepositories
class R2dbcConfiguration : AbstractR2dbcConfiguration() {
#Bean
override fun connectionFactory(): PostgresqlConnectionFactory {
val config = PostgresqlConnectionConfiguration.builder()
.host("localhost")
.port(5432)
.database("postgres")
.username("postgres")
.password("pass123")
.build()
return PostgresqlConnectionFactory(config)
}
}
Found out why this wasnt working.
Save does not persist the changes in the database, to do that you need to call some methods. I called subscribe but because this is a blocking method I ran it either in a let block or a doOnNext
fun createUser(request: ServerRequest): Mono<ServerResponse> {
println("createUser")
val badRequest = ServerResponse.badRequest().build()
val user = request.bodyToMono(User::class.java)
return user
.doOnNext { u -> userRepository.save(u).subscribe() }
.flatMap {
ServerResponse.ok().contentType(APPLICATION_JSON).body(fromValue(it))
}
}

Use interface to pass data in Kotlin

I need to pass data to class from activity. I use interface, but i have problem with initialization.
My class:
class Methods {
fun processingResponse(finalMessage: String) {
var mcontext: Context? = null
var message : Message = Message()
var access = "Access Granted"
var out = "Logged"
var Stateconnect = false
var safetyCheck = 0
if (access in finalMessage) {
val msg = finalMessage.split("=", ":")
accessLevel = msg[0]
sessionId = msg[1].toInt()
safetyCheck = msg[2].toInt()
var namePlc = msg[3]
interfaceData.sendData("Connect")
//Stateconnect = true
} else if (out in finalMessage) {
interfaceData.sendData("Disconnect")
println("log out okey")
}
}}
My interface:
interface SendDataInterface {fun sendData(str: String )}
and My activity:
class LoginIn : AppCompatActivity(), SendDataInterface {
override fun sendData(str: String)
{
var handler = Handler(Looper.getMainLooper())
handler.post( Runnable() {
fun run() {
buttonChange(str)
}
})} fun buttonChange(str : String) {
if (str == "Connect") {
Connection.setBackgroundColor(Color.RED)
Connection.setText("Disconnection")
loadMaintenancePage()
} else if (str == "Disconnect") {
Connection.setBackgroundColor(Color.GREEN)
Connection.setText("Connection")
}
}
}
The error that i have is the interface isn't initialize.
How I can initialize the interface?
You have to create an instance of SendDataInterface in your class Methods.
var interfaceData:SendDtaInterface=Object:SendDtaInterface{
override fun sendData("Connect"){
}
}
interfaceData.sendDat("connect")enter code here

Kotlin equivalent of Swift Expectations/Promises

I am trying to write some UnitTests for my native mobile applications, but have ran into a roadblock in my Android Tests. Specifically, I am struggling to find an example of Kotlin's version of Swift's Expectations/Promises..
I've found examples of Kotlin Promises, but they seem to be way more complicated than needed...
For example, here is a test for the login API function in my iOS project:
func testLogin() {
/// Prepare for login
if CURRENT_USER != nil {
logout()
}
/// Login
let promise = expectation(description: "User is logged in.")
// 1. Given
var isSuccess: Bool = false
// 2. When
User.login(username: maleUsername, password: malePassword, success: {
isSuccess = true
promise.fulfill()
}) { (_, agreeToTerms) in
XCTFail()
}
wait(for: [promise], timeout: maxTimeOut)
// 3. Then
XCTAssertNotNil(CURRENT_USER)
XCTAssertTrue(isSuccess)
/// Logout
logout()
}
This is pretty simple to me. I have an asynchronous method login that has two possible completion blocks: success and failure; and I need to wait for one of them to complete before evaluating. To do this, I create a promise before the call, then I fulfill the promise in the two completion blocks, and I wait for the promise to fulfill before running my assertions.
Now in Kotlin, I have a similar test:
private val loginFragment = LoginFragment()
#Test
fun loginTest() {
val username = ""
val password = ""
// TODO: Create Promise
loginFragment.loginViewModel
.login(username, password)
.observe(loginFragment, Observer {
loginFragment.activity?.onResult(it?.result,
onSuccess = {
// TODO: Fill Promise
},
onValidationError = {
// TODO: Fail Test
})
})
// TODO: Assertions
}
But I can't find an equivalent of swift's promises..
Does one exist in Kotlin? If not, how would I implement a version of my Swift's testLogin method in Kotlin?
You can use Kotlin coroutines, for example:
#Test
fun loginTest() {
val result = runBlocking {
val loginResult = login()
loginResult
}
if (result == "Success") {
// do your work when Success
} else {
// do your work when Error
}
}
suspend fun login(): String = suspendCoroutine { continuation ->
val username = ""
val password = ""
loginFragment.loginViewModel
.login(username, password)
.observe(loginFragment, Observer {
loginFragment.activity?.onResult(it?.result,
onSuccess = {
continuation.resume("Success")
},
onValidationError = {
continuation.resume("Error") // take a look for other methods, e.g. resumeWithException(exception)
})
})
}
To use coroutines you need to add next lines to app's build.gradle file dependencies:
final KOTLIN_COROUTINES_VERSION = '1.0.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLIN_COROUTINES_VERSION"
Hope it will help.
I found this Kotlin Promise lib closer to Swift Promises.
Using this library your test would like.
#Test
fun loginTest() {
val username = ""
val password = ""
Promise<Boolean, Exception> { promise ->
loginFragment.loginViewModel
.login(username, password)
.observe(loginFragment, Observer {
loginFragment.activity?.onResult(it?.result,
onSuccess = {
promise.resolve(true)
},
onValidationError = {
promise.reject(Exception("Login error"))
})
})
}.whenComplete {
when (it) {
is Promise.Result.Success -> {
assert(it.value)
}
}
}
}
I wrote in 20 minutes an implementation that looks like swift's expectation, but he is not sophisticated, use only for simple cases it does the work well.
typealias XCTestExceptionHandler = (KTTestException?) -> Unit
fun TestCase.expectation(description: String): KTTestExpectation {
return KTTestExpectation(description)
}
fun TestCase.waitFor(expectation: KTTestExpectation, timeout: Long, handler: XCTestExceptionHandler) {
expectation.handler = handler
Thread.sleep(timeout)
expectation.timedOut()
}
class KTTestExpectation(private val description: String) {
private var isFulfilled = false
var handler: XCTestExceptionHandler? = null
fun fulfill() {
Handler(Looper.getMainLooper()).postDelayed({
invokeHandlerWith(null)
}, 2)
}
fun timedOut() {
invokeHandlerWith(KTTestException("Timed out: $description"))
}
private fun invokeHandlerWith(error: KTTestException?) {
if (isFulfilled) return
isFulfilled = true
handler?.invoke(error)
error?.let { Assert.fail("Timed out: $description") }
}
}
class KTTestException(message:String): Exception(message)
Using:
fun testExpectation() {
var nb = 0
val expectation = expectation("Test")
MyAsyncFunc {
nb = 5
expectation.fulfill()
}
waitFor(expectation, 1000) { error ->
assertEquals(5, nb)
}
}
If anyone has the courage to convert the original code to Kotlin Here are the links:
https://github.com/apple/swift-corelibs-xctest/blob/main/Sources/XCTest/Public/Asynchronous/XCTWaiter.swift
https://github.com/apple/swift-corelibs-xctest/blob/main/Sources/XCTest/Public/Asynchronous/XCTestExpectation.swift