diff --git a/app/build.gradle b/app/build.gradle index 90d6f37..13f630e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,6 +70,11 @@ dependencies { implementation 'android.arch.core:core-testing' androidTestImplementation 'androidx.test:rules:1.3.0-rc01' + // Mockk + def mockk_ver = "1.10.2" + testImplementation "io.mockk:mockk:$mockk_ver" + androidTestImplementation "io.mockk:mockk-android:$mockk_ver" + //Retrofit and GSON implementation 'com.squareup.retrofit2:retrofit:2.8.1' implementation 'com.squareup.retrofit2:converter-gson:2.8.1' @@ -96,4 +101,6 @@ dependencies { // Circle Image View implementation 'com.mikhaellopez:circularimageview:4.2.0' + // Latest version of JSONObject - needed to pass unit tests + testImplementation 'org.json:json:20180813' } diff --git a/app/src/androidTest/java/com/example/h_mal/stackexchangeusers/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/h_mal/stackexchangeusers/ExampleInstrumentedTest.kt deleted file mode 100644 index a74f85a..0000000 --- a/app/src/androidTest/java/com/example/h_mal/stackexchangeusers/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.h_mal.stackexchangeusers - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.example.h_mal.candyspace", appContext.packageName) - } -} diff --git a/app/src/androidTest/java/com/example/h_mal/stackexchangeusers/data/room/AppDatabaseTest.kt b/app/src/androidTest/java/com/example/h_mal/stackexchangeusers/data/room/AppDatabaseTest.kt new file mode 100644 index 0000000..0ab3a93 --- /dev/null +++ b/app/src/androidTest/java/com/example/h_mal/stackexchangeusers/data/room/AppDatabaseTest.kt @@ -0,0 +1,86 @@ +package com.example.h_mal.stackexchangeusers.data.room + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.matcher.ViewMatchers +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.hamcrest.CoreMatchers +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors + +class AppDatabaseTest{ + @get:Rule + var rule: TestRule = InstantTaskExecutorRule() + + private lateinit var simpleDao: UsersDao + private lateinit var db: AppDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java) + .build() + simpleDao = db.getUsersDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(Exception::class) + fun writeEntryAndReadResponse(){ + // Given + val entity = UserEntity(123) + val latch = CountDownLatch(1) + var userEntity: UserEntity? = null + + // When + simpleDao.saveAllUsers(listOf(entity)) + + // Then + simpleDao.getUser(123).observeForever { + userEntity = it + latch.countDown() + } + + ViewMatchers.assertThat(userEntity, CoreMatchers.equalTo(entity)) + } + + @Test + @Throws(Exception::class) + fun upsertUsersAndReadResponse() = runBlocking{ + // Given + val userEntity = UserEntity(123) + val newUserEntity = UserEntity(456) + var result: List? = null + + // When + simpleDao.saveAllUsers(listOf(userEntity)) + simpleDao.upsertNewUsers(listOf(newUserEntity)) + + // Then + val latch = CountDownLatch(1) + simpleDao.getAllUsers().observeForever { + result = it + latch.countDown() + } + + ViewMatchers.assertThat(newUserEntity, CoreMatchers.equalTo(result?.get(0))) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/model/UserItem.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/model/UserItem.kt new file mode 100644 index 0000000..9188cda --- /dev/null +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/model/UserItem.kt @@ -0,0 +1,27 @@ +package com.example.h_mal.stackexchangeusers.data.model + +import androidx.room.PrimaryKey +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity + +data class UserItem( + val userId: Int?, + val displayName: String?, + val bronze: Int?, + val silver: Int?, + val gold: Int?, + val reputation: Int?, + val creationDate: Int?, + val profileImage: String? +){ + + constructor(userEntity: UserEntity): this( + userEntity.userId, + userEntity.displayName, + userEntity.bronze, + userEntity.silver, + userEntity.gold, + userEntity.reputation, + userEntity.creationDate, + userEntity.profileImage + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/api/ApiClass.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/api/ApiClass.kt index 4834df6..d1ed310 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/api/ApiClass.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/api/ApiClass.kt @@ -2,7 +2,7 @@ package com.example.h_mal.stackexchangeusers.data.network.api import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.NetworkConnectionInterceptor import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.QueryParamsInterceptor -import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse +import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse import okhttp3.OkHttpClient import retrofit2.Response import retrofit2.Retrofit diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/ResponseUnwrap.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/SafeApiCall.kt similarity index 96% rename from app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/ResponseUnwrap.kt rename to app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/SafeApiCall.kt index e525633..22052c2 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/ResponseUnwrap.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/SafeApiCall.kt @@ -5,7 +5,7 @@ import org.json.JSONObject import retrofit2.Response import java.io.IOException -abstract class ResponseUnwrap { +abstract class SafeApiCall { @Suppress("BlockingMethodInNonBlockingContext") suspend fun responseUnwrap( diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/ApiResponse.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/ApiResponse.kt similarity index 67% rename from app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/ApiResponse.kt rename to app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/ApiResponse.kt index 6d4c6b9..2713499 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/ApiResponse.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/ApiResponse.kt @@ -1,4 +1,4 @@ -package com.example.h_mal.stackexchangeusers.data.network.model +package com.example.h_mal.stackexchangeusers.data.network.response data class ApiResponse( val items : List?, diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/BadgeCounts.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/BadgeCounts.kt similarity index 82% rename from app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/BadgeCounts.kt rename to app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/BadgeCounts.kt index d615025..0e56f54 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/BadgeCounts.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/BadgeCounts.kt @@ -1,4 +1,4 @@ -package com.example.h_mal.stackexchangeusers.data.network.model +package com.example.h_mal.stackexchangeusers.data.network.response import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/User.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/User.kt similarity index 93% rename from app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/User.kt rename to app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/User.kt index 4fc791c..b16b9c3 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/model/User.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/network/response/User.kt @@ -1,4 +1,4 @@ -package com.example.h_mal.stackexchangeusers.data.network.model +package com.example.h_mal.stackexchangeusers.data.network.response import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/Repository.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/Repository.kt index 18bb49a..6deae8a 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/Repository.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/Repository.kt @@ -1,14 +1,14 @@ package com.example.h_mal.stackexchangeusers.data.repositories import androidx.lifecycle.LiveData -import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse -import com.example.h_mal.stackexchangeusers.data.network.model.User -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse +import com.example.h_mal.stackexchangeusers.data.network.response.User +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity interface Repository { - fun getUsersFromDatabase(): LiveData> - fun getSingleUserFromDatabase(id: Int): LiveData + fun getUsersFromDatabase(): LiveData> + fun getSingleUserFromDatabase(id: Int): LiveData suspend fun saveUsersToDatabase(users: List) suspend fun getUsersFromApi(username: String): ApiResponse? fun saveCurrentSearchToPrefs(username: String) diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryImpl.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryImpl.kt index d5069fe..9ea14b4 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryImpl.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryImpl.kt @@ -1,33 +1,29 @@ package com.example.h_mal.stackexchangeusers.data.repositories import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass -import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse -import com.example.h_mal.stackexchangeusers.data.network.model.User -import com.example.h_mal.stackexchangeusers.data.network.networkUtils.ResponseUnwrap +import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse +import com.example.h_mal.stackexchangeusers.data.network.response.User +import com.example.h_mal.stackexchangeusers.data.network.networkUtils.SafeApiCall import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider import com.example.h_mal.stackexchangeusers.data.room.AppDatabase -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity const val MILLISECONDS_ONE_MIN = 60000 class RepositoryImpl( private val api: ApiClass, private val database: AppDatabase, private val preference: PreferenceProvider -) : ResponseUnwrap(), Repository { +) : SafeApiCall(), Repository { - // Current list of users in the database override fun getUsersFromDatabase() = database.getUsersDao().getAllUsers() - // retrieving a single user from an ID override fun getSingleUserFromDatabase(id: Int) = database.getUsersDao().getUser(id) - // save a list of users to the room database override suspend fun saveUsersToDatabase(users: List){ - val userList= getUserList(users) + val userList= users.map { UserEntity(it) } database.getUsersDao().upsertNewUsers(userList) } - // fetch users from an api call override suspend fun getUsersFromApi(username: String): ApiResponse? { return when (isSearchValid(username)) { true -> responseUnwrap { api.getUsersFromApi(username) } @@ -35,14 +31,12 @@ class RepositoryImpl( } } - // save current time and current search input into shared prefs override fun saveCurrentSearchToPrefs(username: String){ val time = System.currentTimeMillis() preference.saveLastSavedAt(username, time) } - // boolean response of validity of search - // if the same search is taking place again with a minute return false + // Check if search is valid based on lasted saved private fun isSearchValid(username: String): Boolean { val time = preference.getLastSavedAt(username) ?: return true val currentTime = System.currentTimeMillis() @@ -50,24 +44,4 @@ class RepositoryImpl( return difference > MILLISECONDS_ONE_MIN } - - // map user from api response to userItem entity for database - private fun getUserList(users: List): List { - return users.map { - it.mapToUserItem() - } - } - - private fun User.mapToUserItem(): UserItem { - return UserItem( - userId, - displayName, - badgeCounts?.bronze, - badgeCounts?.silver, - badgeCounts?.gold, - reputation, - creationDate, - profileImage - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/AppDatabase.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/AppDatabase.kt index 9f50354..a010de6 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/AppDatabase.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/AppDatabase.kt @@ -4,10 +4,10 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity @Database( - entities = [UserItem::class], + entities = [UserEntity::class], version = 1 ) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/UsersDao.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/UsersDao.kt index 108821e..a6587f0 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/UsersDao.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/UsersDao.kt @@ -2,27 +2,27 @@ package com.example.h_mal.stackexchangeusers.data.room import androidx.lifecycle.LiveData import androidx.room.* -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity @Dao interface UsersDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun saveAllUsers(users : List) + fun saveAllUsers(users : List) - @Query("SELECT * FROM UserItem") - fun getAllUsers() : LiveData> + @Query("SELECT * FROM UserEntity") + fun getAllUsers() : LiveData> // clear database and add new entries @Transaction - suspend fun upsertNewUsers(users : List){ + suspend fun upsertNewUsers(users : List){ deleteEntries() saveAllUsers(users) } - @Query("DELETE FROM UserItem") + @Query("DELETE FROM UserEntity") suspend fun deleteEntries() - @Query("SELECT * FROM UserItem WHERE userId = :id") - fun getUser(id: Int) : LiveData + @Query("SELECT * FROM UserEntity WHERE userId = :id") + fun getUser(id: Int) : LiveData } \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/entities/UserEntity.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/entities/UserEntity.kt new file mode 100644 index 0000000..412b3fd --- /dev/null +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/entities/UserEntity.kt @@ -0,0 +1,30 @@ +package com.example.h_mal.stackexchangeusers.data.room.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.example.h_mal.stackexchangeusers.data.network.response.User + +@Entity +data class UserEntity( + @PrimaryKey(autoGenerate = false) + val userId: Int, + val displayName: String? = null, + val bronze: Int? = null, + val silver: Int? = null, + val gold: Int? = null, + val reputation: Int? = null, + val creationDate: Int? = null, + val profileImage: String? = null +){ + + constructor(user: User): this( + user.userId!!, + user.displayName, + user.badgeCounts?.bronze, + user.badgeCounts?.silver, + user.badgeCounts?.gold, + user.reputation, + user.creationDate, + user.profileImage + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/entities/UserItem.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/entities/UserItem.kt deleted file mode 100644 index f85b549..0000000 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/data/room/entities/UserItem.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.h_mal.stackexchangeusers.data.room.entities - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class UserItem( - @PrimaryKey(autoGenerate = false) - val userId: Int?, - val displayName: String?, - val bronze: Int?, - val silver: Int?, - val gold: Int?, - val reputation: Int?, - val creationDate: Int?, - val profileImage: String? -) \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/MainViewModel.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/MainViewModel.kt index 8639096..c2b6c64 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/MainViewModel.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/MainViewModel.kt @@ -1,9 +1,12 @@ package com.example.h_mal.stackexchangeusers.ui import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import com.example.h_mal.stackexchangeusers.application.AppClass.Companion.idlingResources +import com.example.h_mal.stackexchangeusers.data.model.UserItem import com.example.h_mal.stackexchangeusers.data.repositories.Repository +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity import com.example.h_mal.stackexchangeusers.utils.Event import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -16,11 +19,19 @@ class MainViewModel( ) : ViewModel() { // livedata for user items in room database - val usersLiveData = repository.getUsersFromDatabase() + val usersLiveData = MutableLiveData>() val operationState = MutableLiveData>() val operationError = MutableLiveData>() + init { + val observer = Observer> { + val list = it.map {entity -> UserItem(entity) } + usersLiveData.postValue(list) + } + repository.getUsersFromDatabase().observeForever (observer) + } + fun getUsers(username: String?){ // validate that search term is not empty diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/MainFragment.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/MainFragment.kt index d2f5bc4..80c8c6f 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/MainFragment.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/MainFragment.kt @@ -9,8 +9,8 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.example.h_mal.stackexchangeusers.R -import com.example.h_mal.stackexchangeusers.application.AppClass.Companion.idlingResources -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.model.UserItem +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity import com.example.h_mal.stackexchangeusers.ui.MainViewModel import com.example.h_mal.stackexchangeusers.utils.onSubmitListener import kotlinx.android.synthetic.main.main_fragment.* @@ -21,11 +21,12 @@ import kotlinx.android.synthetic.main.main_fragment.* * Results for users will be displayed here */ class MainFragment : Fragment() { - /** - * get [MainViewModel] from [MainActivity] (the calling activity) - */ + + // Retrieve view model from main activity val viewModel: MainViewModel by activityViewModels() + lateinit var mAdapter: UserRecyclerViewAdapter + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -42,6 +43,13 @@ class MainFragment : Fragment() { viewModel.getUsers(query.toString().trim()) } } + + mAdapter = UserRecyclerViewAdapter(requireView()) + recycler_view.apply { + layoutManager = LinearLayoutManager(context) + setHasFixedSize(true) + adapter = mAdapter + } } override fun onStart() { @@ -55,18 +63,7 @@ class MainFragment : Fragment() { } private val usersObserver = Observer> { - // create adapter the recycler view - val mAdapter = UserRecyclerViewAdapter( - requireView(), - it - ) - - // setup the recyclerview - recycler_view.apply { - layoutManager = LinearLayoutManager(context) - setHasFixedSize(true) - adapter = mAdapter - } + mAdapter.updateList(it) } } diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/UserRecyclerViewAdapter.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/UserRecyclerViewAdapter.kt index ee9de6a..0f0b8d0 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/UserRecyclerViewAdapter.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/home/UserRecyclerViewAdapter.kt @@ -3,24 +3,33 @@ package com.example.h_mal.stackexchangeusers.ui.main.pages.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.persistableBundleOf import androidx.recyclerview.widget.RecyclerView import com.example.h_mal.stackexchangeusers.R -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.model.UserItem +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity import com.example.h_mal.stackexchangeusers.utils.loadImage import com.example.h_mal.stackexchangeusers.utils.navigateTo import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.simple_item.view.* /** - * Create a Recyclerview adapter used to display [UserItem] + * Create a Recyclerview adapter used to display [UserEntity] * @param parentView is used for context and navigation - * @param items is the list of users parsed from the database */ class UserRecyclerViewAdapter ( - val parentView: View, - val items: List + val parentView: View ): RecyclerView.Adapter(){ + var list = mutableListOf() + + fun updateList(users: List) { + list.clear() + list.addAll(users) + notifyDataSetChanged() + } + + // create a viewholder with [R.layout.simple_item] override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view = LayoutInflater @@ -31,13 +40,13 @@ class UserRecyclerViewAdapter ( } override fun getItemCount(): Int { - return items.size + return list.size } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val view = holder as ItemOne - items[position].let { + list[position].let { view.bindUser(it) } } @@ -53,8 +62,7 @@ class UserRecyclerViewAdapter ( itemView.text2.text = userItem.reputation.toString() itemView.setOnClickListener { - val action = - MainFragmentDirections.toUserProfileFragment(items[position].userId!!) + val action = MainFragmentDirections.toUserProfileFragment(list[layoutPosition].userId!!) parentView.navigateTo(action) } } diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/user/UserProfileFragment.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/user/UserProfileFragment.kt index 3d47700..baf0089 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/user/UserProfileFragment.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/ui/main/pages/user/UserProfileFragment.kt @@ -8,16 +8,14 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import com.example.h_mal.stackexchangeusers.R -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity import com.example.h_mal.stackexchangeusers.ui.MainViewModel import com.example.h_mal.stackexchangeusers.utils.epochToData import com.example.h_mal.stackexchangeusers.utils.loadImage import kotlinx.android.synthetic.main.fragment_user_profile.* /** - * A simple [Fragment] subclass. - * Use the [UserProfileFragment.newInstance] factory method to - * create an instance of this fragment. + * Fragment to show the user overview */ class UserProfileFragment : Fragment() { val viewModel: MainViewModel by activityViewModels() @@ -43,7 +41,7 @@ class UserProfileFragment : Fragment() { viewModel.getSingleUser(id).removeObserver(singleUserObserver) } - private val singleUserObserver = Observer { + private val singleUserObserver = Observer { username.text = it.displayName reputation.text = it.reputation.toString() gold_score.text = it.gold.toString() diff --git a/app/src/main/java/com/example/h_mal/stackexchangeusers/utils/ConverterUtil.kt b/app/src/main/java/com/example/h_mal/stackexchangeusers/utils/ConverterUtil.kt index 9d0b7a8..41c5cf6 100644 --- a/app/src/main/java/com/example/h_mal/stackexchangeusers/utils/ConverterUtil.kt +++ b/app/src/main/java/com/example/h_mal/stackexchangeusers/utils/ConverterUtil.kt @@ -7,7 +7,7 @@ import java.util.* fun epochToData(number: Int?): String { number ?: return "" return try { - val sdf = SimpleDateFormat("dd/MM/yyyy") + val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) val netDate = Date(number.toLong() * 1000) sdf.format(netDate) } catch (e: Exception) { diff --git a/app/src/main/res/layout/fragment_user_profile.xml b/app/src/main/res/layout/fragment_user_profile.xml index 2de1867..607b35d 100644 --- a/app/src/main/res/layout/fragment_user_profile.xml +++ b/app/src/main/res/layout/fragment_user_profile.xml @@ -4,20 +4,21 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_margin="16dp" tools:context=".ui.main.pages.user.UserProfileFragment"> - @@ -64,7 +66,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:text="Bagdes:" + android:text="@string/bagdes" app:layout_constraintRight_toLeftOf="@id/divider" app:layout_constraintTop_toBottomOf="@id/reputation_label" /> @@ -73,16 +75,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:text="Gold - " + android:text="@string/gold" app:layout_constraintLeft_toRightOf="@id/divider" app:layout_constraintTop_toBottomOf="@+id/reputation" /> @@ -91,16 +94,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="6dp" - android:text="Silver - " + android:text="@string/silver" app:layout_constraintLeft_toRightOf="@id/divider" app:layout_constraintTop_toBottomOf="@+id/badges_gold" /> @@ -109,16 +113,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="6dp" - android:text="Bronze - " + android:text="@string/bronze" app:layout_constraintLeft_toRightOf="@id/divider" app:layout_constraintTop_toBottomOf="@+id/badges_silver" /> @@ -127,17 +132,18 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:text="Date Joined:" + android:text="@string/date_joined" app:layout_constraintRight_toLeftOf="@id/divider" app:layout_constraintTop_toBottomOf="@id/badges_bronze" /> diff --git a/app/src/main/res/layout/list_item_layout.xml b/app/src/main/res/layout/list_item_layout.xml deleted file mode 100644 index d25ba32..0000000 --- a/app/src/main/res/layout/list_item_layout.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f1eb2d..d435dbc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,11 @@ Hello blank fragment Enter a valid username + Username: + Reputation: + Bagdes: + Gold - + Silver - + Bronze - + Date Joined: diff --git a/app/src/test/java/com/example/h_mal/stackexchangeusers/ExampleUnitTest.kt b/app/src/test/java/com/example/h_mal/stackexchangeusers/ExampleUnitTest.kt deleted file mode 100644 index 2a5e126..0000000 --- a/app/src/test/java/com/example/h_mal/stackexchangeusers/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.h_mal.stackexchangeusers - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/app/src/test/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/SafeApiCallTest.kt b/app/src/test/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/SafeApiCallTest.kt new file mode 100644 index 0000000..684b335 --- /dev/null +++ b/app/src/test/java/com/example/h_mal/stackexchangeusers/data/network/networkUtils/SafeApiCallTest.kt @@ -0,0 +1,50 @@ +package com.example.h_mal.stackexchangeusers.data.network.networkUtils + +import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.* +import org.junit.Test +import retrofit2.Response +import java.io.IOException +import kotlin.test.assertFailsWith + +class SafeApiCallTest: SafeApiCall(){ + + @Test + fun successfulResponse_SuccessfulOutput() = runBlocking{ + // GIVEN + val mockApiResponse = mockk() + val mockResponse = Response.success(mockApiResponse) + + // WHEN + val result = responseUnwrap { mockResponse } + + // THEN + assertNotNull(result) + assertEquals(mockApiResponse, result) + } + + @Test + fun unsuccessfulResponse_thrownOutput() = runBlocking{ + // GIVEN + val errorMessage = "{\n" + + " \"status_code\": 7,\n" + + " \"error_message\": \"Invalid API key: You must be granted a valid key.\",\n" + + "}" + + val errorResponseBody = errorMessage.toResponseBody("application/json".toMediaTypeOrNull()) + val mockResponse = Response.error(404, errorResponseBody) + + //THEN - assert exception is not null + val ioExceptionReturned = assertFailsWith { + responseUnwrap { mockResponse }!! + } + + assertNotNull(ioExceptionReturned) + assertEquals(ioExceptionReturned.message, "Invalid API key: You must be granted a valid key.") + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryTest.kt b/app/src/test/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryTest.kt index 7891324..6f3d08c 100644 --- a/app/src/test/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryTest.kt +++ b/app/src/test/java/com/example/h_mal/stackexchangeusers/data/repositories/RepositoryTest.kt @@ -1,10 +1,11 @@ package com.example.h_mal.stackexchangeusers.data.repositories import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass -import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse +import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider import com.example.h_mal.stackexchangeusers.data.room.AppDatabase import kotlinx.coroutines.runBlocking +import okhttp3.MediaType.Companion.toMediaTypeOrNull import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before @@ -15,6 +16,7 @@ import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import retrofit2.Response import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody import okio.IOException import kotlin.test.assertFailsWith @@ -49,44 +51,49 @@ class RepositoryTest { Mockito.`when`(prefs.getLastSavedAt(input)).thenReturn(null) // THEN - val getUser = repository.getUsersFromApi(input) - assertNotNull(getUser) - assertEquals(mockApiResponse, getUser) + val apiResponse = repository.getUsersFromApi(input) + assertNotNull(apiResponse) + assertEquals(mockApiResponse, apiResponse) } @Test fun fetchUserFromApi_negativeResponse() = runBlocking { //GIVEN - //mock retrofit error response - val mockBody = mock(ResponseBody::class.java) - val mockRaw = mock(okhttp3.Response::class.java) - val re = Response.error(mockBody, mockRaw) + val input = "12345" + val errorMessage = "{\n" + + " \"status_code\": 7,\n" + + " \"error_message\": \"Invalid API key: You must be granted a valid key.\",\n" + + "}" + + val errorResponseBody = errorMessage.toResponseBody("application/json".toMediaTypeOrNull()) + val mockResponse = Response.error(404, errorResponseBody) //WHEN - Mockito.`when`(api.getUsersFromApi("12345")).then { re } + Mockito.`when`(api.getUsersFromApi(input)).then { mockResponse } //THEN - assert exception is not null val ioExceptionReturned = assertFailsWith { - repository.getUsersFromApi("12345") + repository.getUsersFromApi(input) } assertNotNull(ioExceptionReturned) - assertNotNull(ioExceptionReturned.message) + assertEquals(ioExceptionReturned.message, "Invalid API key: You must be granted a valid key.") } @Test fun fetchUserFromApi_alreadySearched() = runBlocking { // GIVEN + val input = "12345" val mockApiResponse = mock(ApiResponse::class.java) val mockResponse = Response.success(mockApiResponse) //WHEN - Mockito.`when`(api.getUsersFromApi("12345")).thenReturn( + Mockito.`when`(api.getUsersFromApi(input)).thenReturn( mockResponse ) - Mockito.`when`(prefs.getLastSavedAt("12345")).thenReturn(System.currentTimeMillis()) + Mockito.`when`(prefs.getLastSavedAt(input)).thenReturn(System.currentTimeMillis()) // THEN - val getUser = repository.getUsersFromApi("12345") + val getUser = repository.getUsersFromApi(input) assertEquals(getUser, null) } diff --git a/app/src/test/java/com/example/h_mal/stackexchangeusers/ui/main/MainViewModelTest.kt b/app/src/test/java/com/example/h_mal/stackexchangeusers/ui/main/MainViewModelTest.kt index e43127e..b2a0b99 100644 --- a/app/src/test/java/com/example/h_mal/stackexchangeusers/ui/main/MainViewModelTest.kt +++ b/app/src/test/java/com/example/h_mal/stackexchangeusers/ui/main/MainViewModelTest.kt @@ -2,14 +2,11 @@ package com.example.h_mal.stackexchangeusers.ui.main import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse -import com.example.h_mal.stackexchangeusers.data.network.model.User +import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse import com.example.h_mal.stackexchangeusers.data.repositories.Repository -import com.example.h_mal.stackexchangeusers.data.repositories.RepositoryImpl -import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem +import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity import com.example.h_mal.stackexchangeusers.ui.MainViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Before @@ -22,8 +19,6 @@ import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import java.io.IOException import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue class MainViewModelTest { @@ -38,8 +33,11 @@ class MainViewModelTest { @Before fun setUp() { MockitoAnnotations.initMocks(this) - viewModel = MainViewModel(repository) + val mockLiveData = object: LiveData>(){} + Mockito.`when`(repository.getUsersFromDatabase()).thenReturn(mockLiveData) + + viewModel = MainViewModel(repository) } @Test @@ -52,11 +50,8 @@ class MainViewModelTest { //THEN viewModel.getUsers("12345") - viewModel.operationState.observeForever{ - it.getContentIfNotHandled()?.let {result -> - assertFalse { result } - } - } + delay(50) + assertFalse { viewModel.operationState.value?.getContentIfNotHandled()!! } } @Test @@ -65,11 +60,8 @@ class MainViewModelTest { Mockito.`when`(repository.getUsersFromApi("12345")).thenAnswer{ throw IOException("throwed") } // THEN - viewModel.getUsers("fsdfsdf") - viewModel.operationError.observeForever{ - it.getContentIfNotHandled()?.let {result -> - assertEquals(result, "throwed") - } - } + viewModel.getUsers("12345") + delay(50) + assertEquals (viewModel.operationError.value?.getContentIfNotHandled()!!, "throwed") } } \ No newline at end of file