Unit tests added

Query interceptor added
Unit tests created
 - Repository test network
 - Repository test storage
This commit is contained in:
2020-05-15 21:13:05 +01:00
parent 9753312573
commit 7308d3f9df
30 changed files with 554 additions and 199 deletions

View File

@@ -3,11 +3,49 @@
<component name="WizardSettings"> <component name="WizardSettings">
<option name="children"> <option name="children">
<map> <map>
<entry key="vectorWizard"> <entry key="imageWizard">
<value> <value>
<PersistentState /> <PersistentState />
</value> </value>
</entry> </entry>
<entry key="vectorWizard">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="vectorAssetStep">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="clipartAsset">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material_design_icons/navigation/ic_refresh_black_24dp.xml" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
<option name="values">
<map>
<entry key="color" value="ffffff" />
<entry key="outputName" value="ic_refresh_white_24dp" />
<entry key="sourceFile" value="C:\Users\h_mal" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map> </map>
</option> </option>
</component> </component>

Binary file not shown.

View File

@@ -53,6 +53,7 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
//Retrofit and GSON //Retrofit and GSON
implementation 'com.squareup.retrofit2:retrofit:2.6.0' implementation 'com.squareup.retrofit2:retrofit:2.6.0'
@@ -72,6 +73,10 @@ dependencies {
implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1" implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1"
implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1" implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1"
//mockito and livedata testing
testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation 'androidx.arch.core:core-testing:2.1.0'
//Android Room //Android Room
implementation "androidx.room:room-runtime:2.2.0-rc01" implementation "androidx.room:room-runtime:2.2.0-rc01"
implementation "androidx.room:room-ktx:2.2.0-rc01" implementation "androidx.room:room-ktx:2.2.0-rc01"
@@ -83,5 +88,7 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.1.0" implementation "androidx.preference:preference-ktx:1.1.0"
//mock websever for testing retrofit responses
testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
} }

View File

@@ -13,7 +13,7 @@
android:anyDensity="true" /> android:anyDensity="true" />
<application <application
android:name=".mvvm.AppClass" android:name=".mvvm.application.AppClass"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -27,7 +27,7 @@
<meta-data <meta-data
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/currency_kotlin_app_widget_info" /> android:resource="@xml/currency_app_widget_info" />
</receiver> </receiver>
<activity android:name="com.appttude.h_mal.easycc.mvvm.ui.widget.CurrencyAppWidgetConfigureActivityKotlin"> <activity android:name="com.appttude.h_mal.easycc.mvvm.ui.widget.CurrencyAppWidgetConfigureActivityKotlin">
@@ -42,23 +42,7 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".legacy.MainActivityJava"></activity>
<receiver android:name=".legacy.CurrencyAppWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/currency_app_widget_info" />
</receiver>
<activity android:name=".legacy.CurrencyAppWidgetConfigureActivity">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
</application> </application>
</manifest> </manifest>

View File

@@ -1,9 +1,10 @@
package com.appttude.h_mal.easycc.mvvm package com.appttude.h_mal.easycc.mvvm.application
import android.app.Application import android.app.Application
import com.appttude.h_mal.easycc.mvvm.data.Repository.Repository import com.appttude.h_mal.easycc.mvvm.data.repository.RepositoryImpl
import com.appttude.h_mal.easycc.mvvm.data.network.NetworkConnectionInterceptor import com.appttude.h_mal.easycc.mvvm.data.network.NetworkConnectionInterceptor
import com.appttude.h_mal.easycc.mvvm.data.network.api.GetData import com.appttude.h_mal.easycc.mvvm.data.network.QueryInterceptor
import com.appttude.h_mal.easycc.mvvm.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.mvvm.data.prefs.PreferenceProvider import com.appttude.h_mal.easycc.mvvm.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.mvvm.ui.app.MainViewModelFactory import com.appttude.h_mal.easycc.mvvm.ui.app.MainViewModelFactory
import com.appttude.h_mal.easycc.mvvm.ui.widget.WidgetViewModelFactory import com.appttude.h_mal.easycc.mvvm.ui.widget.WidgetViewModelFactory
@@ -15,19 +16,18 @@ import org.kodein.di.generic.instance
import org.kodein.di.generic.provider import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton import org.kodein.di.generic.singleton
class AppClass : Application(), KodeinAware{ class AppClass : Application(), KodeinAware {
override val kodein = Kodein.lazy { override val kodein by Kodein.lazy {
import(androidXModule(this@AppClass)) import(androidXModule(this@AppClass))
bind() from singleton { NetworkConnectionInterceptor(instance()) } bind() from singleton { NetworkConnectionInterceptor(instance()) }
bind() from singleton { GetData(instance()) } bind() from singleton { QueryInterceptor() }
bind() from singleton { CurrencyApi(instance(),instance()) }
bind() from singleton { PreferenceProvider(instance()) } bind() from singleton { PreferenceProvider(instance()) }
bind() from singleton { Repository(instance(), instance(), instance()) } bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
bind() from provider { MainViewModelFactory(instance()) } bind() from provider { MainViewModelFactory(instance()) }
bind() from provider { WidgetViewModelFactory(instance()) } bind() from provider { WidgetViewModelFactory(instance()) }
} }
} }

View File

@@ -1,43 +0,0 @@
package com.appttude.h_mal.easycc.mvvm.data.Repository
import android.content.Context
import com.appttude.h_mal.easycc.BuildConfig
import com.appttude.h_mal.easycc.mvvm.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.mvvm.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.mvvm.data.network.SafeApiRequest
import com.appttude.h_mal.easycc.mvvm.data.network.api.GetData
class Repository (
private val api: GetData,
private val prefs: PreferenceProvider,
context: Context
): SafeApiRequest(){
var ccApiKey = BuildConfig.CC_API_KEY
private val appContext = context.applicationContext
suspend fun getData(s1: String, s2: String): ResponseObject?{
return apiRequest{ api.getCurrencyRate(convertPairsListToString(s1, s2),ccApiKey)}
}
fun getConversionPair(): List<String?> {
return prefs.getConversionPair()
}
fun setConversionPair(s1: String, s2: String){
prefs.saveConversionPair(s1, s2)
}
private fun convertPairsListToString(s1: String, s2: String): String = "${s1.substring(0,3)}_${s2.substring(0,3)}"
fun getArrayList(): Array<String> = appContext.resources.getStringArray(R.array.currency_arrays)
fun getWidgetConversionPairs(id: Int): List<String?> = prefs.getWidgetConversionPair(id)
fun setWidgetConversionPairs(s1: String, s2: String, id: Int) = prefs.saveWidgetConversionPair(s1, s2, id)
fun removeWidgetConversionPairs(id: Int) = prefs.removeWidgetConversion(id)
}

View File

@@ -0,0 +1,30 @@
package com.appttude.h_mal.easycc.mvvm.data.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
class QueryInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original: Request = chain.request()
val originalHttpUrl: HttpUrl = original.url()
val url = originalHttpUrl.newBuilder()
.addQueryParameter("apikey", "a4f93cc2ff05dd772321")
.build()
// Add amended Url back to request
val requestBuilder: Request.Builder = original.newBuilder()
.url(url)
val request: Request = requestBuilder.build()
return chain.proceed(request)
}
}

View File

@@ -1,29 +1,67 @@
package com.appttude.h_mal.easycc.mvvm.data.network package com.appttude.h_mal.easycc.mvvm.data.network
import android.util.Log
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
/**
* This abstract class extract objects from Retrofit [Response]
* or throws IOException if object does not exist
*/
private const val TAG = "SafeApiRequest"
abstract class SafeApiRequest { abstract class SafeApiRequest {
suspend fun<T: Any> apiRequest(call: suspend () -> Response<T>) : T{ suspend fun <T : Any> responseUnwrap(
call: suspend () -> Response<T>
): T {
val response = call.invoke() val response = call.invoke()
if(response.isSuccessful){
return response.body()!!
}else{
val error = response.errorBody()?.string()
val message = StringBuilder() if (response.isSuccessful) {
error?.let{ // return the object within the response body
try{ return response.body()!!
message.append(JSONObject(it).getString("error")) } else {
}catch(e: JSONException){ } // the response was unsuccessful
message.append("\n") // throw IOException error
} throw IOException(errorMessage(response))
message.append("Error Code: ${response.code()}")
throw IOException(message.toString())
} }
} }
private fun <T> errorMessage(errorResponse: Response<T>): String {
val errorBody = errorResponse.errorBody()?.string()
val errorCode = "Error Code: ${errorResponse.code()}"
val errorMessageString = errorBody.getError()
//build a log message to log in console
val log = if (errorMessageString.isNullOrEmpty()){
errorCode
}else{
StringBuilder()
.append(errorCode)
.append("\n")
.append(errorMessageString)
.toString()
}
Log.e("Api Response Error", log)
//return error message
//if null return error code
return errorMessageString ?: errorCode
}
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
private fun String?.getError(): String? {
this?.let {
try {
//convert response to JSON
//extract ["error"] from error body
return JSONObject(it).getString("error")
} catch (e: JSONException) {
Log.e(TAG, e.message)
}
}
return null
}
} }

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.easycc.mvvm.data.network.api
import com.appttude.h_mal.easycc.mvvm.data.network.response.ResponseObject import com.appttude.h_mal.easycc.mvvm.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.mvvm.data.network.NetworkConnectionInterceptor import com.appttude.h_mal.easycc.mvvm.data.network.NetworkConnectionInterceptor
import com.appttude.h_mal.easycc.mvvm.data.network.QueryInterceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
@@ -10,15 +11,17 @@ import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
interface GetData { interface CurrencyApi {
companion object{ companion object{
operator fun invoke( operator fun invoke(
networkConnectionInterceptor: NetworkConnectionInterceptor networkConnectionInterceptor: NetworkConnectionInterceptor,
) : GetData{ queryInterceptor: QueryInterceptor
) : CurrencyApi{
val okkHttpclient = OkHttpClient.Builder() val okkHttpclient = OkHttpClient.Builder()
.addNetworkInterceptor(networkConnectionInterceptor) .addNetworkInterceptor(networkConnectionInterceptor)
.addInterceptor(queryInterceptor)
.build() .build()
return Retrofit.Builder() return Retrofit.Builder()
@@ -26,11 +29,11 @@ interface GetData {
.baseUrl("https://free.currencyconverterapi.com/api/v3/") .baseUrl("https://free.currencyconverterapi.com/api/v3/")
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
.create(GetData::class.java) .create(CurrencyApi::class.java)
} }
} }
@GET("convert?") @GET("convert?")
suspend fun getCurrencyRate(@Query("q") currency: String, @Query("apiKey") api: String): Response<ResponseObject> suspend fun getCurrencyRate(@Query("q") currency: String): Response<ResponseObject>
} }

View File

@@ -7,5 +7,5 @@ class ResponseObject(
@SerializedName("query") @SerializedName("query")
var query : Any, var query : Any,
@SerializedName("results") @SerializedName("results")
var results : Map<String, CurrencyObject> var results : Map<String, CurrencyObject>?
) )

View File

@@ -32,11 +32,11 @@ class PreferenceProvider(
).apply() ).apply()
} }
fun getConversionPair(): List<String?> { fun getConversionPair(): Pair<String?, String?> {
val s1 = getLastConversionOne() val s1 = getLastConversionOne()
val s2 = getLastConversionTwo() val s2 = getLastConversionTwo()
return listOf(s1,s2) return Pair(s1,s2)
} }
private fun getLastConversionOne(): String? { private fun getLastConversionOne(): String? {
@@ -57,11 +57,11 @@ class PreferenceProvider(
).apply() ).apply()
} }
fun getWidgetConversionPair(id: Int): List<String?> { fun getWidgetConversionPair(id: Int): Pair<String?, String?> {
val s1 = getWidgetLastConversionOne(id) val s1 = getWidgetLastConversionOne(id)
val s2 = getWidgetLastConversionTwo(id) val s2 = getWidgetLastConversionTwo(id)
return listOf(s1,s2) return Pair(s1, s2)
} }
private fun getWidgetLastConversionOne(id: Int): String? { private fun getWidgetLastConversionOne(id: Int): String? {

View File

@@ -0,0 +1,23 @@
package com.appttude.h_mal.easycc.mvvm.data.repository
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.mvvm.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.mvvm.utils.convertPairsListToString
interface Repository {
suspend fun getData(s1: String, s2: String): ResponseObject
fun getConversionPair(): Pair<String?, String?>
fun setConversionPair(s1: String, s2: String)
fun getArrayList(): Array<String>
fun getWidgetConversionPairs(id: Int): Pair<String?, String?>
fun setWidgetConversionPairs(s1: String, s2: String, id: Int)
fun removeWidgetConversionPairs(id: Int)
}

View File

@@ -0,0 +1,48 @@
package com.appttude.h_mal.easycc.mvvm.data.repository
import android.content.Context
import com.appttude.h_mal.easycc.BuildConfig
import com.appttude.h_mal.easycc.mvvm.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.mvvm.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.mvvm.data.network.SafeApiRequest
import com.appttude.h_mal.easycc.mvvm.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.mvvm.utils.convertPairsListToString
class RepositoryImpl (
private val api: CurrencyApi,
private val prefs: PreferenceProvider,
context: Context
):Repository, SafeApiRequest(){
private val appContext = context.applicationContext
override suspend fun getData(s1: String, s2: String
): ResponseObject{
val currencyPair = convertPairsListToString(s1, s2)
return responseUnwrap{
api.getCurrencyRate(currencyPair)}
}
override fun getConversionPair(): Pair<String?, String?> {
return prefs.getConversionPair()
}
override fun setConversionPair(s1: String, s2: String){
prefs.saveConversionPair(s1, s2)
}
override fun getArrayList(): Array<String> =
appContext.resources.getStringArray(R.array.currency_arrays)
override fun getWidgetConversionPairs(id: Int): Pair<String?, String?> =
prefs.getWidgetConversionPair(id)
override fun setWidgetConversionPairs(s1: String, s2: String, id: Int) =
prefs.saveWidgetConversionPair(s1, s2, id)
override fun removeWidgetConversionPairs(id: Int) =
prefs.removeWidgetConversion(id)
}

View File

@@ -7,15 +7,14 @@ import android.util.Log
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.ActivityMainBinding import com.appttude.h_mal.easycc.databinding.ActivityMainBinding
import com.appttude.h_mal.easycc.utils.DisplayToast import com.appttude.h_mal.easycc.mvvm.utils.DisplayToast
import com.appttude.h_mal.easycc.utils.clearEditText import com.appttude.h_mal.easycc.mvvm.utils.clearEditText
import com.appttude.h_mal.easycc.utils.hideView import com.appttude.h_mal.easycc.mvvm.utils.hideView
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein import org.kodein.di.android.kodein

View File

@@ -3,13 +3,12 @@ package com.appttude.h_mal.easycc.mvvm.ui.app
import android.widget.EditText import android.widget.EditText
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.appttude.h_mal.easycc.mvvm.data.Repository.Repository import com.appttude.h_mal.easycc.mvvm.data.repository.Repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.NumberFormat
class MainViewModel( class MainViewModel(
private val repository: Repository private val repository: Repository
@@ -18,14 +17,20 @@ class MainViewModel(
private val defaultValue by lazy { repository.getArrayList()[0] } private val defaultValue by lazy { repository.getArrayList()[0] }
private val conversionPairs by lazy { repository.getConversionPair() } private val conversionPairs by lazy { repository.getConversionPair() }
var rateIdFrom: String? = conversionPairs[0] ?: defaultValue var rateIdFrom: String? = conversionPairs.first ?: defaultValue
var rateIdTo: String? = conversionPairs[1] ?: defaultValue var rateIdTo: String? = conversionPairs.second ?: defaultValue
var topVal: String? = null var topVal: String? = null
var bottomVal: String? = null var bottomVal: String? = null
var rateListener: RateListener? = null var rateListener: RateListener? = null
//operation results livedata based on outcome of operation
val operationSuccess = MutableLiveData<Boolean>()
val operationFailed = MutableLiveData<String>()
val currencyRate = MutableLiveData<Double>()
private var conversionRate: Double = 0.00 private var conversionRate: Double = 0.00
fun getExchangeRate(){ fun getExchangeRate(){
@@ -40,15 +45,17 @@ class MainViewModel(
val exchangeResponse = repository.getData(rateIdFrom!!, rateIdTo!!) val exchangeResponse = repository.getData(rateIdFrom!!, rateIdTo!!)
repository.setConversionPair(rateIdFrom!!, rateIdTo!!) repository.setConversionPair(rateIdFrom!!, rateIdTo!!)
exchangeResponse?.results?.iterator()?.next()?.value?.let { exchangeResponse.results?.iterator()?.next()?.value?.let {
rateListener?.onSuccess() rateListener?.onSuccess()
conversionRate = it.value conversionRate = it.value
return@launch return@launch
} }
rateListener?.onFailure("Failed to retrieve rate")
}catch(e: IOException){ }catch(e: IOException){
rateListener?.onFailure(e.message!!) rateListener?.onFailure(e.message ?: "Currency Retrieval failed")
return@launch
} }
rateListener?.onFailure("Failed to retrieve rate")
} }
} }

View File

@@ -2,11 +2,11 @@ package com.appttude.h_mal.easycc.mvvm.ui.app
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.easycc.mvvm.data.Repository.Repository import com.appttude.h_mal.easycc.mvvm.data.repository.RepositoryImpl
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class MainViewModelFactory ( class MainViewModelFactory (
private val repository: Repository private val repository: RepositoryImpl
): ViewModelProvider.NewInstanceFactory(){ ): ViewModelProvider.NewInstanceFactory(){
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {

View File

@@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModelProviders
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding
import com.appttude.h_mal.easycc.mvvm.ui.app.RateListener import com.appttude.h_mal.easycc.mvvm.ui.app.RateListener
import com.appttude.h_mal.easycc.utils.DisplayToast import com.appttude.h_mal.easycc.mvvm.utils.DisplayToast
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein import org.kodein.di.android.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance

View File

@@ -3,16 +3,16 @@ package com.appttude.h_mal.easycc.mvvm.ui.widget
import android.app.PendingIntent import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import com.appttude.h_mal.easycc.legacy.MainActivityJava
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.mvvm.data.Repository.Repository import com.appttude.h_mal.easycc.mvvm.data.repository.RepositoryImpl
import com.appttude.h_mal.easycc.mvvm.data.network.NetworkConnectionInterceptor import com.appttude.h_mal.easycc.mvvm.ui.app.MainActivity
import com.appttude.h_mal.easycc.mvvm.data.network.api.GetData import com.appttude.h_mal.easycc.mvvm.utils.transformIntToArray
import com.appttude.h_mal.easycc.mvvm.data.prefs.PreferenceProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -21,34 +21,51 @@ import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import java.io.IOException import java.io.IOException
/** /**
* Implementation of App Widget functionality. * Implementation of App Widget functionality.
* App Widget Configuration implemented in [CurrencyAppWidgetConfigureActivityKotlin] * App Widget Configuration implemented in [CurrencyAppWidgetConfigureActivityKotlin]
*/ */
private const val TAG = "CurrencyAppWidgetKotlin"
class CurrencyAppWidgetKotlin : AppWidgetProvider() { class CurrencyAppWidgetKotlin : AppWidgetProvider() {
//DI with kodein to use in CurrencyAppWidgetKotlin
private val kodein = LateInitKodein() private val kodein = LateInitKodein()
private val repository : Repository by kodein.instance() private val repository : RepositoryImpl by kodein.instance()
//update trigger either on timed update or from from first start
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein Log.i(TAG,"onUpdate() appWidgetIds = ${appWidgetIds.size}")
// There may be multiple widgets active, so update all of them // There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) { for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId) updateAppWidget(context, appWidgetManager, appWidgetId)
} }
super.onUpdate(context, appWidgetManager, appWidgetIds)
} }
override fun onDeleted(context: Context, appWidgetIds: IntArray) { override fun onDeleted(context: Context, appWidgetIds: IntArray) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
// When the user deletes the widget, delete the preference associated with it. // When the user deletes the widget, delete the preference associated with it.
for (appWidgetId in appWidgetIds) { for (appWidgetId in appWidgetIds) {
repository.removeWidgetConversionPairs(appWidgetId) repository.removeWidgetConversionPairs(appWidgetId)
} }
super.onDeleted(context, appWidgetIds)
} }
override fun onEnabled(context: Context) { override fun onEnabled(context: Context) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
// Enter relevant functionality for when the first widget is created // Enter relevant functionality for when the first widget is created
AppWidgetManager.getInstance(context).apply {
val thisAppWidget = ComponentName(context.packageName, CurrencyAppWidgetKotlin::class.java.name)
val appWidgetIds = getAppWidgetIds(thisAppWidget)
onUpdate(context, this, appWidgetIds)
}
super.onEnabled(context)
}
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null){ return }
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
super.onReceive(context, intent)
} }
override fun onDisabled(context: Context) { override fun onDisabled(context: Context) {
@@ -57,58 +74,75 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() {
} }
fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) { private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
//todo: get value from repository
val stringList = repository.getWidgetConversionPairs(appWidgetId) val stringList = repository.getWidgetConversionPairs(appWidgetId)
val s1 = stringList[0] val s1 = stringList.first
val s2 = stringList[1] val s2 = stringList.second
// Construct the RemoteViews object // Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.currency_app_widget) val views = RemoteViews(context.packageName, R.layout.currency_app_widget)
views.setTextViewText(R.id.exchangeName, "Rates")
views.setTextViewText(R.id.exchangeRate, "not set")
//todo: async task to get rate
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
val response = repository.getData(s1!!.substring(0,3),s2!!.substring(0,3)) val response = repository.getData(s1!!.substring(0,3),s2!!.substring(0,3))
response?.results?.iterator()?.next()?.value?.let { response.results?.iterator()?.next()?.value?.let {
val titleString = "${it.fr}${it.to}" val titleString = "${it.fr}${it.to}"
views.setTextViewText(R.id.exchangeName, titleString) views.setTextViewText(R.id.exchangeName, titleString)
views.setTextViewText(R.id.exchangeRate, it.value.toString()) views.setTextViewText(R.id.exchangeRate, it.value.toString())
} }
}catch (io : IOException){ }catch (io : IOException){
Log.i("WidgetClass",io.message ?: "Failed")
Toast.makeText(context,io.message, Toast.LENGTH_LONG).show() Toast.makeText(context,io.message, Toast.LENGTH_LONG).show()
}finally { }finally {
// Instruct the widget manager to update the widget setUpdateIntent(context, appWidgetId).let {
appWidgetManager.updateAppWidget(appWidgetId, views) //set the pending intent to the icon
views.setImageViewResource(R.id.refresh_icon, R.drawable.ic_refresh_white_24dp)
val opacity = 0.3f //opacity = 0: fully transparent, opacity = 1: no transparancy views.setOnClickPendingIntent(R.id.refresh_icon, it)
val backgroundColor = 0x000000 //background color (here black)
views.setInt(R.id.widget_view, "setBackgroundColor", (opacity * 0xFF).toInt() shl 24 or backgroundColor)
val clickIntentTemplate = Intent(context, MainActivityJava::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
putExtra("parse_1", s1)
putExtra("parse_2", s2)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
} }
val configPendingIntent = PendingIntent.getActivity(context, 0, clickIntentTemplate, PendingIntent.FLAG_UPDATE_CURRENT) val clickIntentTemplate = clickingIntent(context, s1, s2)
val configPendingIntent =
PendingIntent.getActivity(
context, appWidgetId, clickIntentTemplate,
PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.widget_view, configPendingIntent) views.setOnClickPendingIntent(R.id.widget_view, configPendingIntent)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
} }
} }
} }
fun setupRepository(context: Context): Repository { private fun setUpdateIntent(context: Context, appWidgetId: Int): PendingIntent? {
val networkInterceptor = NetworkConnectionInterceptor(context) //Create update intent for refresh icon
val getData = GetData(networkInterceptor) val updateIntent = Intent(
val prefs = PreferenceProvider(context) context, CurrencyAppWidgetKotlin::class.java).apply {
return Repository(getData,prefs,context) action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, transformIntToArray(appWidgetId))
}
//add previous intent to this pending intent
return PendingIntent.getBroadcast(
context,
appWidgetId,
updateIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
}
private fun clickingIntent(
context: Context,
s1: String?, s2: String?
): Intent {
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
putExtra("parse_1", s1)
putExtra("parse_2", s2)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
} }
} }

View File

@@ -6,11 +6,13 @@ import android.appwidget.AppWidgetManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.mvvm.utils.transformIntToArray
import kotlinx.android.synthetic.main.confirm_dialog.* import kotlinx.android.synthetic.main.confirm_dialog.*
/* /**
widget for when submitting the completed selections * Dialog created when submitting the completed selections
* in [CurrencyAppWidgetConfigureActivityKotlin]
*/ */
class WidgetSubmitDialog( class WidgetSubmitDialog(
private val activity: Activity, private val activity: Activity,
@@ -21,26 +23,32 @@ class WidgetSubmitDialog(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.confirm_dialog) setContentView(R.layout.confirm_dialog)
// layer behind dialog to be transparent
// requestWindowFeature(Window.FEATURE_NO_TITLE)
window!!.setBackgroundDrawableResource(android.R.color.transparent) window!!.setBackgroundDrawableResource(android.R.color.transparent)
// Dialog cannot be cancelled by clicking away
setCancelable(false) setCancelable(false)
//todo: amend widget text
confirm_text.text = StringBuilder().append("Create widget for ") confirm_text.text = StringBuilder().append("Create widget for ")
.append(viewModel.getWidgetStringName()) .append(viewModel.getWidgetStringName())
.append("?").toString() .append("?").toString()
confirm_yes.setOnClickListener { confirm_yes.setOnClickListener {
viewModel.setWidgetStored() // It is the responsibility of the configuration activity to update the app widget
// Send update broadcast to widget app class
Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE,
null,
context,
CurrencyAppWidgetKotlin::class.java).apply {
// Save current widget pairs
viewModel.setWidgetStored()
// Put current app widget ID into extras and send broadcast
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, transformIntToArray(appWidgetId) )
activity.sendBroadcast(this)
}
val intent = Intent(context, CurrencyAppWidgetKotlin::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, IntArray(appWidgetId))
context.sendBroadcast(intent)
// Make sure we pass back the original appWidgetId // Make sure we pass back the original appWidgetId
val resultValue = Intent() val resultValue = activity.intent
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
activity.setResult(Activity.RESULT_OK, resultValue) activity.setResult(Activity.RESULT_OK, resultValue)
activity.finish() activity.finish()

View File

@@ -4,11 +4,11 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.appttude.h_mal.easycc.mvvm.data.Repository.Repository import com.appttude.h_mal.easycc.mvvm.data.repository.RepositoryImpl
import com.appttude.h_mal.easycc.mvvm.ui.app.RateListener import com.appttude.h_mal.easycc.mvvm.ui.app.RateListener
class WidgetViewModel( class WidgetViewModel(
private val repository: Repository private val repository: RepositoryImpl
) : ViewModel(){ ) : ViewModel(){
var rateListener: RateListener? = null var rateListener: RateListener? = null
@@ -22,10 +22,9 @@ class WidgetViewModel(
appWidgetId = appId appWidgetId = appId
val widgetString = getWidgetStored(appId) val widgetString = getWidgetStored(appId)
if (widgetString.isNotEmpty()){ rateIdFrom.value = widgetString.first
rateIdFrom.value = widgetString[0] rateIdTo.value = widgetString.second
rateIdTo.value = widgetString[1]
}
} }
fun selectCurrencyOnClick(view: View){ fun selectCurrencyOnClick(view: View){
@@ -80,15 +79,5 @@ class WidgetViewModel(
private fun String.trimToThree() = this.substring(0,3) private fun String.trimToThree() = this.substring(0,3)
private fun arrayEntry(s: String?): String? {
val strings = repository.getArrayList()
var returnString: String? = strings[0]
for (string in strings) {
if (s == string.substring(0, 3)) {
returnString = string
}
}
return returnString
}
} }

View File

@@ -2,11 +2,11 @@ package com.appttude.h_mal.easycc.mvvm.ui.widget
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.easycc.mvvm.data.Repository.Repository import com.appttude.h_mal.easycc.mvvm.data.repository.RepositoryImpl
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class WidgetViewModelFactory ( class WidgetViewModelFactory (
private val repository: Repository private val repository: RepositoryImpl
): ViewModelProvider.NewInstanceFactory(){ ): ViewModelProvider.NewInstanceFactory(){
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {

View File

@@ -0,0 +1,15 @@
package com.appttude.h_mal.easycc.mvvm.utils
fun transformIntToArray(int: Int): IntArray{
return intArrayOf(int)
}
fun String.trimToThree(): String{
if (this.length > 3){
return this.substring(0, 3)
}
return this
}
fun convertPairsListToString(s1: String, s2: String): String =
"${s1.trimToThree()}_${s2.trimToThree()}"

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.easycc.utils package com.appttude.h_mal.easycc.mvvm.utils
import android.content.Context import android.content.Context
import android.view.View import android.view.View
@@ -15,4 +15,4 @@ fun View.hideView(vis : Boolean){
fun Context.DisplayToast(message: String){ fun Context.DisplayToast(message: String){
Toast.makeText(this, message, Toast.LENGTH_LONG).show() Toast.makeText(this, message, Toast.LENGTH_LONG).show()
} }

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View File

@@ -15,15 +15,17 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="6dp" android:layout_margin="12dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/confirm_text" android:id="@+id/confirm_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_margin="8dp" android:layout_marginLeft="12dp"
android:layout_marginRight="36dp"
android:layout_marginBottom="24dp"
android:text="Create widget for AUDGBP?" android:text="Create widget for AUDGBP?"
android:textColor="@color/colour_five" /> android:textColor="@color/colour_five" />
@@ -37,7 +39,11 @@
android:id="@+id/confirm_yes" android:id="@+id/confirm_yes"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:textStyle="bold"
android:text="@android:string/yes" android:text="@android:string/yes"
android:textColor="@color/colour_five" /> android:textColor="@color/colour_five" />
@@ -46,6 +52,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:textStyle="bold"
android:text="@android:string/no" android:text="@android:string/no"
android:textColor="@color/colour_five" /> android:textColor="@color/colour_five" />

View File

@@ -2,23 +2,38 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_view" android:id="@+id/widget_view"
android:layout_width="match_parent" android:layout_width="match_parent"
tools:layout_width="110dp"
android:layout_height="72dp" android:layout_height="72dp"
android:orientation="vertical"> android:orientation="vertical"
android:background="#4D000000">
<RelativeLayout
<TextView
android:id="@+id/exchangeName"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginBottom="3dp"
android:layout_weight="1" android:layout_weight="1"
android:autoSizeMaxTextSize="100sp" android:layout_marginBottom="3dp">
android:autoSizeMinTextSize="8sp" <TextView
android:autoSizeStepGranularity="2sp" android:id="@+id/exchangeName"
android:autoSizeTextType="uniform" android:layout_width="match_parent"
android:gravity="center" android:layout_height="match_parent"
android:textColor="#ffffff" android:gravity="center"
tools:text="AUDGBP" /> android:autoSizeMaxTextSize="100sp"
android:autoSizeMinTextSize="6sp"
android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform"
android:textColor="#ffffff"
android:text="Rate not set"
tools:text="AUDGBP" />
<ImageView
android:id="@+id/refresh_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:src="@drawable/ic_refresh_white_24dp"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignBottom="@id/exchangeName"
android:adjustViewBounds="true"/>
</RelativeLayout>
<TextView <TextView
android:id="@+id/exchangeRate" android:id="@+id/exchangeRate"
@@ -32,5 +47,5 @@
android:gravity="center" android:gravity="center"
android:textColor="#ffffff" android:textColor="#ffffff"
android:textStyle="bold" android:textStyle="bold"
tools:text="0.56" /> tools:text="0.526462" />
</LinearLayout> </LinearLayout>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.appttude.h_mal.easycc.legacy.CurrencyAppWidgetConfigureActivity" android:configure="com.appttude.h_mal.easycc.mvvm.ui.widget.CurrencyAppWidgetConfigureActivityKotlin"
android:initialKeyguardLayout="@layout/currency_app_widget" android:initialKeyguardLayout="@layout/currency_app_widget"
android:initialLayout="@layout/currency_app_widget" android:initialLayout="@layout/currency_app_widget"
android:minHeight="40dp" android:minHeight="40dp"
@@ -9,7 +9,7 @@
android:minResizeWidth="40dp" android:minResizeWidth="40dp"
android:previewImage="@drawable/easyycc_widget_preview" android:previewImage="@drawable/easyycc_widget_preview"
android:resizeMode="horizontal" android:resizeMode="horizontal"
android:updatePeriodMillis="86400000" android:updatePeriodMillis="21600000"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

View File

@@ -7,5 +7,5 @@
android:minHeight="40dp" android:minHeight="40dp"
android:previewImage="@drawable/example_appwidget_preview" android:previewImage="@drawable/example_appwidget_preview"
android:resizeMode="horizontal|vertical" android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000" android:updatePeriodMillis="3600000"
android:widgetCategory="home_screen|keyguard"></appwidget-provider> android:widgetCategory="home_screen|keyguard"/>

View File

@@ -0,0 +1,86 @@
package com.appttude.h_mal.easycc.repository
import android.content.Context
import com.appttude.h_mal.easycc.BuildConfig
import com.appttude.h_mal.easycc.mvvm.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.mvvm.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.mvvm.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.mvvm.data.repository.Repository
import com.appttude.h_mal.easycc.mvvm.data.repository.RepositoryImpl
import com.appttude.h_mal.easycc.mvvm.utils.convertPairsListToString
import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import retrofit2.Response
import java.io.IOException
import kotlin.test.assertFailsWith
class RepositoryNetworkTest{
lateinit var repository: Repository
@Mock
lateinit var api: CurrencyApi
@Mock
lateinit var prefs: PreferenceProvider
@Mock
lateinit var context: Context
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
repository = RepositoryImpl(api, prefs, context)
}
@Test
fun getRateFromApi_positiveResponse() = runBlocking {
//GIVEN - Create query string
val s1 = "AUD - Australian Dollar"
val s2 = "GBP - British Pound"
val query = convertPairsListToString(s1, s2)
//create a successful retrofit response
val mockCurrencyResponse = mock(ResponseObject::class.java)
val re = Response.success(mockCurrencyResponse)
//WHEN - loginApiRequest to return a successful response
Mockito.`when`(api.getCurrencyRate(query)).thenReturn(re)
//THEN - the unwrapped login response contains the correct values
val currencyResponse = repository.getData(s1,s2)
assertNotNull(currencyResponse)
assertEquals(currencyResponse, mockCurrencyResponse)
}
@Test
fun loginUser_negativeResponse() = runBlocking {
//GIVEN
val s1 = "AUD - Australian Dollar"
val s2 = "GBP - British Pound"
val query = convertPairsListToString(s1, s2)
//mock retrofit error response
val mockBody = mock(ResponseBody::class.java)
val mockRaw = mock(okhttp3.Response::class.java)
val re = Response.error<String>(mockBody, mockRaw)
//WHEN
Mockito.`when`(api.getCurrencyRate(query)).thenAnswer { re }
//THEN - assert exception is not null
val ioExceptionReturned = assertFailsWith<IOException> {
repository.getData(s1, s2)
}
assertNotNull(ioExceptionReturned)
assertNotNull(ioExceptionReturned.message)
}
}

View File

@@ -0,0 +1,62 @@
package com.appttude.h_mal.easycc.repository
import android.content.Context
import com.appttude.h_mal.easycc.mvvm.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.mvvm.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.mvvm.data.repository.Repository
import com.appttude.h_mal.easycc.mvvm.data.repository.RepositoryImpl
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
class RepositoryStorageTest {
lateinit var repository: Repository
@Mock
lateinit var api: CurrencyApi
@Mock
lateinit var prefs: PreferenceProvider
@Mock
lateinit var context: Context
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
repository = RepositoryImpl(api, prefs, context)
}
@Test
fun saveAndRetrieve_PositiveResponse() {
//GIVEN
val s1 = "AUD - Australian Dollar"
val s2 = "GBP - British Pound"
val pair = Pair(s1, s2)
repository.setConversionPair(s1, s2)
//WHEN
Mockito.`when`(prefs.getConversionPair()).thenReturn(pair)
//THEN
assertEquals(pair, repository.getConversionPair())
}
@Test
fun saveAndRetrieveCredentials_PositiveResponse() {
//GIVEN
val s1 = "AUD - Australian Dollar"
val s2 = "GBP - British Pound"
val id = 1234
val pair = Pair(s1, s2)
repository.setWidgetConversionPairs("forename", "Surname", id)
//WHEN
Mockito.`when`(prefs.getWidgetConversionPair(id)).thenReturn(pair)
//THEN
assertEquals(pair, repository.getWidgetConversionPairs(id))
}
}