From 220afa04cf32321067d3ce81cf2915ca16665806 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Fri, 8 Sep 2023 14:59:54 +0100 Subject: [PATCH] - room database migration in progress --- app/build.gradle | 13 ++ .../h_mal/farmr/application/TestAppClass.kt | 16 +- .../farmr/data/room/RoomMigrationTest.kt | 68 +++++++++ .../h_mal/farmr/base/BaseApplication.kt | 3 +- .../appttude/h_mal/farmr/data/Repository.kt | 6 +- .../h_mal/farmr/data/RepositoryImpl.kt | 69 ++++++--- .../farmr/data/legacydb/ShiftsDbHelper.kt | 4 +- .../h_mal/farmr/data/room/AppDatabase.kt | 51 +++++++ .../h_mal/farmr/data/room/ShiftDao.kt | 37 +++++ .../data/room/converters/DateConverter.kt | 19 +++ .../data/room/converters/TimeConverter.kt | 23 +++ .../farmr/data/room/entity/ShiftEntity.kt | 34 +++++ .../farmr/data/room/migrations/Migrations.kt | 14 ++ .../h_mal/farmr/di/ShiftApplication.kt | 6 +- .../h_mal/farmr/model/DatabaseShift.kt | 11 -- .../com/appttude/h_mal/farmr/model/Shift.kt | 38 ++++- .../h_mal/farmr/ui/FragmentAddItem.kt | 25 ++-- .../h_mal/farmr/viewmodel/MainViewModel.kt | 59 +++++--- .../farmr/viewmodel/SubmissionViewModel.kt | 75 +++++----- .../h_mal/farmr/data/RepositoryImplTest.kt | 139 ++++++++++++++++-- .../data/room/converters/TimeConverterTest.kt | 26 ++++ .../appttude/h_mal/farmr/utils/testUtils.kt | 86 +++++------ .../farmr/viewmodel/InfoViewModelTest.kt | 7 +- .../farmr/viewmodel/MainViewModelTest.kt | 54 +++++-- 24 files changed, 695 insertions(+), 188 deletions(-) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/room/RoomMigrationTest.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/room/AppDatabase.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/room/ShiftDao.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/DateConverter.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverter.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/room/entity/ShiftEntity.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/room/migrations/Migrations.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverterTest.kt diff --git a/app/build.gradle b/app/build.gradle index 3ba231c..8574611 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'kotlin-kapt' def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD") def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD") @@ -17,6 +18,12 @@ android { versionName "2.1" testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' vectorDrawables.useSupportLibrary = true + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": + "$projectDir/schemas".toString()] + } + } } signingConfigs { release { @@ -33,6 +40,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + sourceSets { + androidTest.assets.srcDirs += + files("$projectDir/schemas".toString()) + } useLibrary 'android.test.mock' } @@ -77,6 +88,8 @@ dependencies { / * Room database * / runtimeOnly "androidx.room:room-runtime:$ROOM_VERSION" implementation "androidx.room:room-ktx:$ROOM_VERSION" + kapt "androidx.room:room-compiler:$ROOM_VERSION" + androidTestImplementation "android.arch.persistence.room:testing:1.1.1" / *Kodein Dependency Injection * / implementation "org.kodein.di:kodein-di-generic-jvm:$KODEIN_VERSION" implementation "org.kodein.di:kodein-di-framework-android-x:$KODEIN_VERSION" diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt index 616c325..fe76238 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt @@ -1,17 +1,19 @@ package com.appttude.h_mal.farmr.application +import androidx.room.Room import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.platform.app.InstrumentationRegistry import com.appttude.h_mal.farmr.base.BaseApplication import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.data.room.AppDatabase import com.appttude.h_mal.farmr.model.Shift class TestAppClass : BaseApplication() { private val idlingResources = CountingIdlingResource("Data_loader") - lateinit var database: LegacyDatabase + lateinit var database: AppDatabase lateinit var preferenceProvider: PreferenceProvider override fun onCreate() { @@ -19,9 +21,9 @@ class TestAppClass : BaseApplication() { IdlingRegistry.getInstance().register(idlingResources) } - override fun createDatabase(): LegacyDatabase { - database = - LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver) + override fun createDatabase(): AppDatabase { + database = Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java) + .build() return database } @@ -30,9 +32,9 @@ class TestAppClass : BaseApplication() { return preferenceProvider } - fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift) - fun addShiftsToDatabase(shifts: List) = shifts.forEach { addToDatabase(it) } - fun clearDatabase() = database.deleteAllShiftsInDatabase() + fun addToDatabase(shift: Shift) = database.getShiftDao().upsertFullShift(shift.convertToShiftEntity()) + fun addShiftsToDatabase(shifts: List) = shifts.map { it.convertToShiftEntity() }.let { database.getShiftDao().upsertListOfFullShift(it) } + fun clearDatabase() = database.getShiftDao().deleteAllShifts() fun cleanPrefs() = preferenceProvider.clearPrefs() } \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/room/RoomMigrationTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/room/RoomMigrationTest.kt new file mode 100644 index 0000000..f22b512 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/room/RoomMigrationTest.kt @@ -0,0 +1,68 @@ +package com.appttude.h_mal.farmr.data.room + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.testing.MigrationTestHelper +import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract +import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper +import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME +import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5 +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.utils.getShifts +import org.junit.Rule +import org.junit.Test +import java.io.IOException + + +class RoomMigrationTest { + private val TEST_DB = "migration-test" + + @get:Rule + val testHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java.canonicalName + ) + + @Test + @Throws(IOException::class) + fun migrationFrom2To3_containsCorrectData() { + // Create the database in version 4 + val db = testHelper.createDatabase(TEST_DB, 4) + // Insert some data + getShifts().forEach { + db.insert( + ShiftsContract.ShiftsEntry.TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, ContentValues().apply { + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE, it.type.type) + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, it.description) + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE, it.date) + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN, it.timeIn ?: "00:00") + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT, it.timeOut ?: "00:00") + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION, it.duration ?: 0.00f) + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK, it.breakMins ?: 0) + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT, it.units ?: 0.00f) + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE, it.rateOfPay) + put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY, it.totalPay) + }) + } + //Prepare for the next version + db.close() + + // Re-open the database with version 5 and provide MIGRATION_4_5 + // and MIGRATION_4_5 as the migration process. + testHelper.runMigrationsAndValidate( + TEST_DB, 5, + true, MIGRATION_4_5 + ) + + // MigrationTestHelper automatically verifies the schema + //changes, but not the data validity + // Validate that the data was migrated properly. + val dbUser: User = getMigratedRoomDatabase().userDao().getUser() + assertEquals(dbUser.getId(), USER.getId()) + assertEquals(dbUser.getUserName(), USER.getUserName()) + // The date was missing in version 2, so it should be null in + //version 3 + assertEquals(dbUser.getDate(), null) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt index 055e5f3..05ffd22 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt @@ -4,6 +4,7 @@ import android.app.Application import com.appttude.h_mal.farmr.data.RepositoryImpl import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.data.room.AppDatabase import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory import org.kodein.di.Kodein import org.kodein.di.KodeinAware @@ -26,6 +27,6 @@ abstract class BaseApplication() : Application(), KodeinAware { bind() from provider { ApplicationViewModelFactory(instance()) } } - abstract fun createDatabase(): LegacyDatabase + abstract fun createDatabase(): AppDatabase abstract fun createPrefs(): PreferenceProvider } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt index db97694..9feb218 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt @@ -1,6 +1,8 @@ package com.appttude.h_mal.farmr.data +import androidx.lifecycle.LiveData import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.Sortable @@ -8,8 +10,8 @@ import com.appttude.h_mal.farmr.model.Sortable interface Repository { fun insertShiftIntoDatabase(shift: Shift): Boolean fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean - fun readShiftsFromDatabase(): List? - fun readSingleShiftFromDatabase(id: Long): ShiftObject? + fun readShiftsFromDatabase(): LiveData> + fun readSingleShiftFromDatabase(id: Long): ShiftEntity? fun deleteSingleShiftFromDatabase(id: Long): Boolean fun deleteAllShiftsFromDatabase(): Boolean fun retrieveSortAndOrderFromPref(): Pair diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt index 3af7652..27d4929 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt @@ -1,50 +1,77 @@ package com.appttude.h_mal.farmr.data +import androidx.lifecycle.LiveData +import androidx.room.Room import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.data.room.AppDatabase +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.utils.dateStringIsValid +import com.appttude.h_mal.farmr.utils.timeStringIsValid class RepositoryImpl( - private val legacyDatabase: LegacyDatabase, + roomDatabase: AppDatabase, private val preferenceProvider: PreferenceProvider ): Repository { + + private val shiftDao = roomDatabase.getShiftDao() + override fun insertShiftIntoDatabase(shift: Shift): Boolean { - return legacyDatabase.insertShiftDataIntoDatabase(shift) != null + val shiftEntity = shift.convertToShiftEntity() + return shiftDao.upsertFullShift(shiftEntity) > 0 } override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean { - return legacyDatabase.updateShiftDataIntoDatabase( - id = id, - typeString = shift.type.type, - descriptionString = shift.description, - dateString = shift.date, - timeInString = shift.timeIn ?: "", - timeOutString = shift.timeOut ?: "", - duration = shift.duration ?: 0f, - breaks = shift.breakMins ?: 0, - units = shift.units ?: 0f, - payRate = shift.rateOfPay, - totalPay = shift.totalPay - ) == 1 + if (shift.description.isBlank() || shift.description.trim().length < 3) { + throw IllegalArgumentException("description required") + } + if (!shift.date.dateStringIsValid()) { + throw IllegalArgumentException("date required") + } + shift.timeIn?.takeIf { !it.timeStringIsValid() }?.let { + throw IllegalArgumentException("time in required") + } + shift.timeOut?.takeIf { !it.timeStringIsValid() }?.let { + throw IllegalArgumentException("time out required") + } + shift.breakMins?.takeIf { it < 0 }?.let { + throw IllegalArgumentException("break required") + } + if (shift.timeIn != null || shift.timeOut != null) { + if (shift.duration == null) throw IllegalArgumentException("Duration required") + } + shift.units?.takeIf { it < 0 }?.let { + throw IllegalArgumentException("Units required") + } + if (shift.rateOfPay < 0) { + throw IllegalArgumentException("Rate of pay required") + } + if (shift.totalPay < 0) { + throw IllegalArgumentException("Total pay required") + } + val shiftEntity = shift.convertToShiftEntity(id) + return shiftDao.upsertFullShift(shiftEntity) > 0 } - override fun readShiftsFromDatabase(): List? { - return legacyDatabase.readShiftsFromDatabase() + override fun readShiftsFromDatabase(): LiveData> { + return shiftDao.getAllFullShift() } - override fun readSingleShiftFromDatabase(id: Long): ShiftObject? { - return legacyDatabase.readSingleShiftWithId(id) + override fun readSingleShiftFromDatabase(id: Long): ShiftEntity? { + return shiftDao.getCurrentFullShiftSingle(id) } override fun deleteSingleShiftFromDatabase(id: Long): Boolean { - return legacyDatabase.deleteSingleShift(id) == 1 + return shiftDao.deleteShift(id) == 1 } override fun deleteAllShiftsFromDatabase(): Boolean { - return legacyDatabase.deleteAllShiftsInDatabase() > 0 + return shiftDao.deleteAllShifts() > 0 } override fun retrieveSortAndOrderFromPref(): Pair { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt index 4918518..93fd525 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt @@ -33,10 +33,10 @@ class ShiftsDbHelper(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAM } companion object { - private const val DATABASE_NAME = "shifts.db" + const val DATABASE_NAME = "shifts.db" private const val DATABASE_VERSION = 4 private const val DEFAULT_TEXT = "Hourly" - private const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " (" + const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " (" + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " TEXT NOT NULL, " + ShiftsEntry.COLUMN_SHIFT_DATE + " DATE NOT NULL, " + ShiftsEntry.COLUMN_SHIFT_TIME_IN + " TIME NOT NULL, " diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/room/AppDatabase.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/room/AppDatabase.kt new file mode 100644 index 0000000..19f3799 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/room/AppDatabase.kt @@ -0,0 +1,51 @@ +package com.appttude.h_mal.farmr.data.room + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper +import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME +import com.appttude.h_mal.farmr.data.room.converters.DateConverter +import com.appttude.h_mal.farmr.data.room.converters.TimeConverter +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity +import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5 + +@Database( + entities = [ShiftEntity::class], + version = 5, + exportSchema = true +) +@TypeConverters(DateConverter::class, TimeConverter::class) +abstract class AppDatabase : RoomDatabase() { + + abstract fun getShiftDao(): ShiftDao + + companion object { + + @Volatile + private var instance: AppDatabase? = null + private val LOCK = Any() + + // create an instance of room database or use previously created instance + operator fun invoke(context: Context) = instance ?: synchronized(LOCK) { + instance ?: buildDatabase(context).also { + instance = it + } + } + + private fun buildDatabase(context: Context) = + Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + DATABASE_NAME + ).addMigrations(MIGRATION_4_5) + .addTypeConverter(DateConverter()) + .addTypeConverter(TimeConverter()) + .build() + } +} + diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/room/ShiftDao.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/room/ShiftDao.kt new file mode 100644 index 0000000..30dc09d --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/room/ShiftDao.kt @@ -0,0 +1,37 @@ +package com.appttude.h_mal.farmr.data.room + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.DeleteTable +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity + +@Dao +interface ShiftDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun upsertFullShift(item: ShiftEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun upsertListOfFullShift(items: List) + + @Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1") + fun getCurrentFullShift(shiftId: Long): LiveData + + @Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1") + fun getCurrentFullShiftSingle(shiftId: Long): ShiftEntity? + + @Query("SELECT * FROM shifts") + fun getAllFullShift(): LiveData> + + @Query("DELETE FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId") + fun deleteShift(shiftId: Long): Int + + @Query("DELETE FROM shifts") + fun deleteAllShifts(): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/DateConverter.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/DateConverter.kt new file mode 100644 index 0000000..59f7400 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/DateConverter.kt @@ -0,0 +1,19 @@ +package com.appttude.h_mal.farmr.data.room.converters + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import java.sql.Date + +@ProvidedTypeConverter +class DateConverter { + @TypeConverter + fun toDate(dateString: String): Date { + // Convert yyyy-MM-dd into date + return Date.valueOf(dateString) + } + + @TypeConverter + fun fromDate(date: Date): String { + return date.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverter.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverter.kt new file mode 100644 index 0000000..d39e62b --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverter.kt @@ -0,0 +1,23 @@ +package com.appttude.h_mal.farmr.data.room.converters + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import java.sql.Date +import java.sql.Time +import java.time.Instant + +@ProvidedTypeConverter +class TimeConverter { + @TypeConverter + fun toTime(timeString: String?): Time { + // Convert HH:mm into date + timeString?.let { return Time.valueOf("$timeString:00") } + return Time.valueOf("00:00:00") + } + + @TypeConverter + fun fromTime(time: Time): String { + // Return string in format of HH:mm + return time.toString().substring(0,5) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/room/entity/ShiftEntity.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/room/entity/ShiftEntity.kt new file mode 100644 index 0000000..ec98326 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/room/entity/ShiftEntity.kt @@ -0,0 +1,34 @@ +package com.appttude.h_mal.farmr.data.room.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.TABLE_NAME +import com.appttude.h_mal.farmr.model.ShiftType +import java.sql.Date +import java.sql.Time + + +@Entity(tableName = TABLE_NAME) +data class ShiftEntity( + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) val description: String, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DATE, typeAffinity = ColumnInfo.UNDEFINED) val date: Date, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_IN, typeAffinity = ColumnInfo.TEXT) val timeIn: Time, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_OUT, typeAffinity = ColumnInfo.TEXT) val timeOut: Time, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_BREAK) val breakMins: Int? = 0, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DURATION, typeAffinity = ColumnInfo.REAL, defaultValue = "0") val duration: Float = 0f, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TYPE) val type: String, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_UNIT, typeAffinity = ColumnInfo.REAL) val units: Float? = 0f, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_PAYRATE, typeAffinity = ColumnInfo.REAL) val payRate: Float? = 0f, + @ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TOTALPAY, typeAffinity = ColumnInfo.REAL) val totalPay: Float? = 0f, + @PrimaryKey + @ColumnInfo(name = ShiftsEntry._ID) val id: Long? = 0, +) { + fun convertToShiftObject(): ShiftObject { + return ShiftObject( + id ?: 0, type, description, date.toString(), timeIn.toString(), timeOut.toString(), duration, breakMins ?: 0, units ?: 0f, payRate ?: 0f, totalPay ?: 0f + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/room/migrations/Migrations.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/room/migrations/Migrations.kt new file mode 100644 index 0000000..9362054 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/room/migrations/Migrations.kt @@ -0,0 +1,14 @@ +package com.appttude.h_mal.farmr.data.room.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper + +val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS '" + ShiftsDbHelper.SQL_CREATE_PRODUCTS_TABLE_2 + "'"); + database.execSQL("ALTER TABLE `shifts` ADD COLUMN `date` TEXT NOT NULL") + + // CREATE TABLE IF NOT EXISTS (`description` TEXT NOT NULL, , `timein` TEXT NOT NULL, `timeout` TEXT NOT NULL, `break` INTEGER, `duration` REAL NOT NULL DEFAULT 0, `shifttype` TEXT NOT NULL, `unit` REAL, `payrate` REAL, `totalpay` REAL, `_id` INTEGER, PRIMARY KEY(`_id`)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt index cc4b998..b1a0d43 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt @@ -1,13 +1,15 @@ package com.appttude.h_mal.farmr.di +import androidx.room.RoomDatabase import com.appttude.h_mal.farmr.base.BaseApplication import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.data.room.AppDatabase class ShiftApplication: BaseApplication() { - override fun createDatabase(): LegacyDatabase { - return LegacyDatabase(contentResolver) + override fun createDatabase(): AppDatabase { + return AppDatabase(this) } override fun createPrefs() = PreferenceProvider(this) diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt deleted file mode 100644 index 4120cdc..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.appttude.h_mal.farmr.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class EntityItem( - @PrimaryKey(autoGenerate = false) - val id: String, - val shift: Shift -) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt index 4df1de5..ba74f4d 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt @@ -1,5 +1,8 @@ package com.appttude.h_mal.farmr.model +import com.appttude.h_mal.farmr.data.room.converters.DateConverter +import com.appttude.h_mal.farmr.data.room.converters.TimeConverter +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity import com.appttude.h_mal.farmr.utils.calculateDuration import com.appttude.h_mal.farmr.utils.formatToTwoDp @@ -15,6 +18,39 @@ data class Shift( val rateOfPay: Float, val totalPay: Float ) { + + fun convertToShiftEntity(): ShiftEntity { + val timeConverter = TimeConverter() + return ShiftEntity( + description = description, + type = type.type, + date = DateConverter().toDate(date), + timeIn = timeConverter.toTime(timeIn), + timeOut = timeConverter.toTime(timeOut), + duration = duration ?: 0f, + breakMins = breakMins ?: 0, + units = units ?: 0f, + payRate = rateOfPay, + totalPay = totalPay + ) + } + + fun convertToShiftEntity(id: Long): ShiftEntity { + val timeConverter = TimeConverter() + return ShiftEntity( + id = id, + description = description, + type = type.type, + date = DateConverter().toDate(date), + timeIn = timeConverter.toTime(timeIn), + timeOut = timeConverter.toTime(timeOut), + duration = duration ?: 0f, + breakMins = breakMins ?: 0, + units = units ?: 0f, + payRate = rateOfPay, + totalPay = totalPay + ) + } companion object { // Invocation for Hourly operator fun invoke( @@ -60,6 +96,4 @@ data class Shift( (units * rateOfPay).formatToTwoDp() ) } - - } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index 1b9caf5..ffcb0f3 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -13,6 +13,8 @@ import androidx.core.widget.doAfterTextChanged import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BackPressedListener import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.data.room.converters.DateConverter +import com.appttude.h_mal.farmr.data.room.converters.TimeConverter import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.ID @@ -31,6 +33,9 @@ import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), RadioGroup.OnCheckedChangeListener, BackPressedListener { + private val dateConverter = DateConverter() + private val timeConverter = TimeConverter() + private lateinit var mHourlyRadioButton: RadioButton private lateinit var mPieceRadioButton: RadioButton private lateinit var mLocationEditText: EditText @@ -126,41 +131,41 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ // Since we are editing a shift lets load the shift data into the views viewModel.getCurrentShift(arguments!!.getLong(ID))?.run { mLocationEditText.setText(description) - mDateEditText.setText(date) + mDateEditText.setText(dateConverter.fromDate(date)) // Set types mType = ShiftType.getEnumByType(type) mDescription = description - mDate = date - mPayRate = rateOfPay + mDate = dateConverter.fromDate(date) + mPayRate = payRate!! when (ShiftType.getEnumByType(type)) { ShiftType.HOURLY -> { mHourlyRadioButton.isChecked = true mPieceRadioButton.isChecked = false - mTimeInEditText.setText(timeIn) - mTimeOutEditText.setText(timeOut) + mTimeInEditText.setText(timeConverter.fromTime(timeIn)) + mTimeOutEditText.setText(timeConverter.fromTime(timeOut)) mBreakEditText.setText(breakMins.toString()) val durationText = "${duration.formatToTwoDpString()} Hours" mDurationTextView.text = durationText // Set fields - mTimeIn = timeIn - mTimeOut = timeOut + mTimeIn = timeConverter.fromTime(timeIn) + mTimeOut = timeConverter.fromTime(timeOut) mBreaks = breakMins } ShiftType.PIECE -> { mHourlyRadioButton.isChecked = false mPieceRadioButton.isChecked = true - mUnitEditText.setText(units.formatToTwoDpString()) + mUnitEditText.setText(units?.formatToTwoDpString()) // Set piece rate units mUnits = units } } - mPayRateEditText.setText(rateOfPay.formatAsCurrencyString()) - mTotalPayTextView.text = totalPay.formatAsCurrencyString() + mPayRateEditText.setText(payRate.formatAsCurrencyString()) + mTotalPayTextView.text = totalPay?.formatAsCurrencyString() calculateTotalPay() } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index d63fca2..1919ed1 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -1,7 +1,6 @@ package com.appttude.h_mal.farmr.viewmodel import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.legacydb.ShiftObject @@ -16,6 +15,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Sortable @@ -37,25 +37,28 @@ class MainViewModel( private val repository: Repository ) : ShiftViewModel(repository) { - private val _shiftLiveData = MutableLiveData>() - private val shiftLiveData: LiveData> = _shiftLiveData + private val shiftLiveData: LiveData> = repository.readShiftsFromDatabase() private var mSort: Sortable = Sortable.ID private var mOrder: Order = Order.ASCENDING - private val observer = Observer> { - it?.let { - val result = it.applyFilters().sortList(mSort, mOrder) - onSuccess(result) - } + private val observer = Observer> { + it?.let { updateFiltrationAndPostResults(it) } } init { - // Load shifts into live data when view model has been instantiated - refreshLiveData() shiftLiveData.observeForever(observer) } + private fun updateFiltrationAndPostResults(entities: List) { + val result = entities.mapToShiftObjects().applyFilters().sortList(mSort, mOrder) + onSuccess(result) + } + + private fun List.mapToShiftObjects(): List { + return map { i -> i.convertToShiftObject() } + } + private fun List.applyFilters(): List { val filter = getFiltrationDetails() @@ -75,14 +78,21 @@ class MainViewModel( } } - private fun comparedStringsContains(first: String?, second: String?): Boolean { - first?.let { - (second?.contains(it))?.let { c -> return c } + /* + * Check if string compareWith contains compareAgainst + */ + private fun comparedStringsContains(compareWith: String?, compareAgainst: String?): Boolean { + compareWith?.let { + (compareAgainst?.contains(it))?.let { c -> return c } } - return comparedStrings(first, second) + return comparedStrings(compareWith, compareAgainst) } + /* + * check if date [compareWith] fall between two dates + * if fromDate or toDate is null then it will take today's date for comparison + */ private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? { val first = fromDate?.convertDateString() val second = toDate?.convertDateString() @@ -96,7 +106,9 @@ class MainViewModel( return compareDate.after(first) && compareDate.before(second) } - + /* + * When view-model is cleared we stop observing any livedata + */ override fun onCleared() { shiftLiveData.removeObserver(observer) super.onCleared() @@ -132,7 +144,7 @@ class MainViewModel( var totalUnits = 0f var totalPay = 0f var lines = 0 - _shiftLiveData.value?.applyFilters()?.forEach { + shiftLiveData.value?.mapToShiftObjects()?.applyFilters()?.forEach { lines += 1 totalDuration += it.duration when (ShiftType.getEnumByType(it.type)) { @@ -156,16 +168,12 @@ class MainViewModel( fun deleteShift(id: Long) { if (!repository.deleteSingleShiftFromDatabase(id)) { onError("Failed to delete shift") - } else { - refreshLiveData() } } fun deleteAllShifts() { if (!repository.deleteAllShiftsFromDatabase()) { onError("Failed to delete all shifts from database") - } else { - refreshLiveData() } } @@ -194,8 +202,12 @@ class MainViewModel( return stringBuilder.toString() } + /* + * After operations such as updating filtering or sorting + * we update the data we sent to the ui + */ fun refreshLiveData() { - repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) } + shiftLiveData.value?.let { updateFiltrationAndPostResults(it) } } fun clearFilters() { @@ -204,6 +216,9 @@ class MainViewModel( refreshLiveData() } + /* + * Build a .csv file to be exported + */ fun createExcelSheet(file: File): File? { val wbSettings = WorkbookSettings().apply { locale = Locale("en", "EN") @@ -232,7 +247,7 @@ class MainViewModel( return null } val sortAndOrder = getSortAndOrder() - val data = shiftLiveData.value!!.applyFilters() + val data = shiftLiveData.value!!.mapToShiftObjects().applyFilters() .sortList(sortAndOrder.first, sortAndOrder.second) var currentRow = 0 val cells = data.map { shift -> diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt index 322ca40..b72fa46 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt @@ -1,6 +1,8 @@ package com.appttude.h_mal.farmr.viewmodel import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.room.converters.DateConverter +import com.appttude.h_mal.farmr.data.room.converters.TimeConverter import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Success @@ -17,6 +19,9 @@ class SubmissionViewModel( private val repository: Repository ) : ShiftViewModel(repository) { + private val dateConverter = DateConverter() + private val timeConverter = TimeConverter() + fun insertHourlyShift( description: String, date: String, @@ -26,7 +31,7 @@ class SubmissionViewModel( breakMins: Int?, ) { // Validate inputs from the edit texts - (description.length > 3).validateField { + (description.trim().length > 3).validateField { onError("Description length should be longer") return } @@ -53,7 +58,7 @@ class SubmissionViewModel( val result = insertShiftIntoDatabase( ShiftType.HOURLY, - description, + description.trim(), date, rateOfPay.formatToTwoDp(), timeIn, @@ -73,7 +78,7 @@ class SubmissionViewModel( rateOfPay: Float ) { // Validate inputs from the edit texts - (description.length > 3).validateField { + (description.trim().length > 3).validateField { onError("Description length should be longer") return } @@ -92,7 +97,7 @@ class SubmissionViewModel( val result = insertShiftIntoDatabase( type = ShiftType.PIECE, - description = description, + description = description.trim(), date = date, rateOfPay = rateOfPay.formatToTwoDp(), null, @@ -177,72 +182,72 @@ class SubmissionViewModel( breakMins: Int? = null, units: Float? = null, ): Boolean { - val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() + val currentShift = repository.readSingleShiftFromDatabase(id) ?: throw IOException("Cannot update shift as it does not exist") + val mDate = date ?: dateConverter.fromDate(currentShift.date) + val mTimeIn = timeIn ?: timeConverter.fromTime(currentShift.timeIn) + val mTimeOut = timeOut ?: timeConverter.fromTime(currentShift.timeIn) + val shift = when (type) { ShiftType.HOURLY -> { // Shift type has changed so mandatory fields for hourly shift are now required as well - val insertTimeIn = - (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted") - val insertTimeOut = - (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted") Shift( description = description ?: currentShift.description, - date = date ?: currentShift.date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, + date = mDate, + timeIn = mTimeIn, + timeOut = mTimeOut, breakMins = breakMins ?: currentShift.breakMins, - rateOfPay = rateOfPay ?: currentShift.rateOfPay + rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f ) } ShiftType.PIECE -> { // Shift type has changed so mandatory fields for piece rate shift are now required as well - val insertUnits = (units ?: currentShift.units) - ?: throw IOException("Units must be inserted for piece rate shifts") Shift( description = description ?: currentShift.description, - date = date ?: currentShift.date, - units = insertUnits, - rateOfPay = rateOfPay ?: currentShift.rateOfPay + date = mDate, + units = (units ?: currentShift.units) ?: 0f, + rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f ) } else -> { if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { // Updates to description or date field - currentShift.copy( - description = description ?: currentShift.description, - date = date ?: currentShift.date, + Shift( + ShiftType.getEnumByType(currentShift.type), + description ?: currentShift.description, + mDate, + mTimeIn, + mTimeOut, + currentShift.duration, + currentShift.breakMins, + currentShift.units, + currentShift.payRate ?: 0f, + currentShift.totalPay ?: 0f ) } else { // Updating shifts where shift type has remained the same - when (currentShift.type) { + when (ShiftType.getEnumByType(currentShift.type)) { ShiftType.HOURLY -> { - val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException( - "No time in inserted" - ) - val insertTimeOut = (timeOut ?: currentShift.timeOut) - ?: throw IOException("No time out inserted") Shift( description = description ?: currentShift.description, - date = date ?: currentShift.date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, + date = mDate, + timeIn = mTimeIn, + timeOut = mTimeOut, breakMins = breakMins ?: currentShift.breakMins, - rateOfPay = rateOfPay ?: currentShift.rateOfPay + rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f ) } ShiftType.PIECE -> { val insertUnits = (units ?: currentShift.units) - ?: throw IOException("Units must be inserted for piece rate shifts") Shift( description = description ?: currentShift.description, - date = date ?: currentShift.date, - units = insertUnits, - rateOfPay = rateOfPay ?: currentShift.rateOfPay + date = mDate, + units = (insertUnits) ?: 0f, + rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f ) } } diff --git a/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt index 4688aa6..d78a8bb 100644 --- a/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt +++ b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt @@ -1,24 +1,38 @@ package com.appttude.h_mal.farmr.data +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.data.room.AppDatabase +import com.appttude.h_mal.farmr.data.room.ShiftDao +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity +import com.appttude.h_mal.farmr.model.Shift import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk +import kotlinx.coroutines.runBlocking import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyLong +import java.io.IOException import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertIs class RepositoryImplTest { + @get:Rule + val rule = InstantTaskExecutorRule() private lateinit var repository: RepositoryImpl - @MockK - lateinit var db: LegacyDatabase + @RelaxedMockK + lateinit var db: AppDatabase @MockK lateinit var prefs: PreferenceProvider @@ -32,20 +46,123 @@ class RepositoryImplTest { @Test fun readDatabase_validResponse() { // Arrange - val elements = listOf( - mockk { every { id } returns anyLong() }, - mockk { every { id } returns anyLong() }, - mockk { every { id } returns anyLong() }, - mockk { every { id } returns anyLong() } - ) + val liveData = mockk>>() //Act - every { db.readShiftsFromDatabase() } returns elements + every { db.getShiftDao().getAllFullShift() } returns liveData // Assert val result = repository.readShiftsFromDatabase() - assertIs>(result) - assertEquals(result.first().id, anyLong()) + assertIs>>(result) + assertEquals(result, liveData) + } + + @Test + fun updateShift_invalidDescription_validThrow() { + // Arrange + val id = anyLong() + val shift = mockk() + + //Act + val emptyDescExceptionReturned = assertFailsWith { + every { shift.description } returns "" + repository.updateShiftIntoDatabase(id, shift) + } + val untrimmedDescExceptionReturned = assertFailsWith { + every { shift.description } returns "DQ " + repository.updateShiftIntoDatabase(id, shift) + } + + // Assert + assertEquals(emptyDescExceptionReturned.message, "description required") + assertEquals(untrimmedDescExceptionReturned.message, "description required") + } + + @Test + fun updateShift_invalidDate_validThrow() { + // Arrange + val id = anyLong() + val shift = mockk() + + //Act + every { shift.description } returns "Valid desc" + val emptyDateExceptionReturned = assertFailsWith { + every { shift.date } returns "" + repository.updateShiftIntoDatabase(id, shift) + } + val untrimmedDateExceptionReturned = assertFailsWith { + every { shift.description } returns "2022-03-02 " + repository.updateShiftIntoDatabase(id, shift) + } + val wrongFormatDateExceptionReturned = assertFailsWith { + every { shift.description } returns "02-03-2020" + repository.updateShiftIntoDatabase(id, shift) + } + val wrongFormatDateExceptionReturned2 = assertFailsWith { + every { shift.description } returns "2022/03/02" + repository.updateShiftIntoDatabase(id, shift) + } + + // Assert + assertEquals(emptyDateExceptionReturned.message, "date required") + assertEquals(untrimmedDateExceptionReturned.message, "date required") + assertEquals(wrongFormatDateExceptionReturned.message, "date required") + assertEquals(wrongFormatDateExceptionReturned2.message, "date required") + } + + @Test + fun updateShift_invalidTimeInAndTimeOut_validThrow() { + // Arrange + val id = anyLong() + val shift = mockk() + + //Act + every { shift.description } returns "Valid desc" + every { shift.date } returns "2020-06-05" + val emptyTimeInExceptionReturned = assertFailsWith { + every { shift.timeIn } returns "" + repository.updateShiftIntoDatabase(id, shift) + } + val untrimmedTimeInExceptionReturned = assertFailsWith { + every { shift.timeIn } returns "14:04 " + repository.updateShiftIntoDatabase(id, shift) + } + val wrongFormatTimeInExceptionReturned = assertFailsWith { + every { shift.timeIn } returns "14 04" + repository.updateShiftIntoDatabase(id, shift) + } + val wrongFormatTimeInExceptionReturned2 = assertFailsWith { + every { shift.timeIn } returns "1404" + repository.updateShiftIntoDatabase(id, shift) + } + every { shift.timeIn } returns "14:00" + val emptyTimeOutExceptionReturned = assertFailsWith { + every { shift.timeOut } returns "" + repository.updateShiftIntoDatabase(id, shift) + } + val untrimmedTimeOutExceptionReturned = assertFailsWith { + every { shift.timeOut } returns "14:04 " + repository.updateShiftIntoDatabase(id, shift) + } + val wrongFormatTimeOutExceptionReturned = assertFailsWith { + every { shift.timeOut } returns "14 04" + repository.updateShiftIntoDatabase(id, shift) + } + val wrongFormatTimeOutExceptionReturned2 = assertFailsWith { + every { shift.timeOut } returns "1404" + repository.updateShiftIntoDatabase(id, shift) + } + + // Assert + assertEquals(emptyTimeInExceptionReturned.message, "time in required") + assertEquals(untrimmedTimeInExceptionReturned.message, "time in required") + assertEquals(wrongFormatTimeInExceptionReturned.message, "time in required") + assertEquals(wrongFormatTimeInExceptionReturned2.message, "time in required") + + assertEquals(emptyTimeOutExceptionReturned.message, "time out required") + assertEquals(untrimmedTimeOutExceptionReturned.message, "time out required") + assertEquals(wrongFormatTimeOutExceptionReturned.message, "time out required") + assertEquals(wrongFormatTimeOutExceptionReturned2.message, "time out required") } } \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverterTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverterTest.kt new file mode 100644 index 0000000..4660d6b --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/data/room/converters/TimeConverterTest.kt @@ -0,0 +1,26 @@ +package com.appttude.h_mal.farmr.data.room.converters + +import org.junit.Assert.* +import org.junit.Before + +import org.junit.Test +import java.sql.Time + +class TimeConverterTest { + + private val converter = TimeConverter() + + @Test + fun toTime() { + val str = "16:04" + val time = converter.toTime(str) + assertEquals(time.toString(), "$str:00") + } + + @Test + fun fromTime() { + val str = "16:04" + val time = converter.fromTime(Time.valueOf("16:04:00")) + assertEquals(time, str) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt index e04e3ce..ca74318 100644 --- a/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt +++ b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt @@ -2,7 +2,7 @@ package com.appttude.h_mal.farmr.utils import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity import com.appttude.h_mal.farmr.model.ShiftType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -42,108 +42,108 @@ fun sleep(millis: Long = 1000) { } fun getShifts() = listOf( - ShiftObject( - ArgumentMatchers.anyLong(), - ShiftType.HOURLY.type, + ShiftEntity( "Day one", "2023-08-01", "12:00", "13:00", - 1f, ArgumentMatchers.anyInt(), + 1f, + ShiftType.HOURLY.type, ArgumentMatchers.anyFloat(), 10f, - 10f - ), - ShiftObject( + 10f, ArgumentMatchers.anyLong(), - ShiftType.HOURLY.type, + ), + ShiftEntity( "Day two", "2023-08-02", "12:00", "13:00", - 1f, ArgumentMatchers.anyInt(), + 1f, + ShiftType.HOURLY.type, ArgumentMatchers.anyFloat(), 10f, - 10f - ), - ShiftObject( + 10f, ArgumentMatchers.anyLong(), - ShiftType.HOURLY.type, + ), + ShiftEntity( "Day three", "2023-08-03", "12:00", - "13:00", - 1f, + "14:30", 30, + 2f, + ShiftType.HOURLY.type, ArgumentMatchers.anyFloat(), 10f, - 5f - ), - ShiftObject( + 20f, ArgumentMatchers.anyLong(), - ShiftType.HOURLY.type, + ), + ShiftEntity( "Day four", "2023-08-04", "12:00", - "13:00", - 1f, + "14:30", 30, + 2f, + ShiftType.HOURLY.type, ArgumentMatchers.anyFloat(), 10f, - 5f - ), - ShiftObject( + 20f, ArgumentMatchers.anyLong(), - ShiftType.PIECE.type, + ), + ShiftEntity( "Day five", "2023-08-05", ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), - ArgumentMatchers.anyFloat(), ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + ShiftType.PIECE.type, 1f, 10f, - 10f - ), - ShiftObject( + 10f, ArgumentMatchers.anyLong(), - ShiftType.PIECE.type, + ), + ShiftEntity( "Day six", "2023-08-06", ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), - ArgumentMatchers.anyFloat(), ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + ShiftType.PIECE.type, 1f, 10f, - 10f - ), - ShiftObject( + 10f, ArgumentMatchers.anyLong(), - ShiftType.PIECE.type, + ), + ShiftEntity( "Day seven", "2023-08-07", ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), - ArgumentMatchers.anyFloat(), ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + ShiftType.PIECE.type, 1f, 10f, - 10f - ), - ShiftObject( + 10f, ArgumentMatchers.anyLong(), - ShiftType.PIECE.type, + ), + ShiftEntity( "Day eight", "2023-08-08", ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), - ArgumentMatchers.anyFloat(), ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + ShiftType.PIECE.type, 1f, 10f, - 10f + 10f, + ArgumentMatchers.anyLong(), ), ) \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt index 3fb73c0..3a537cb 100644 --- a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt @@ -2,6 +2,7 @@ package com.appttude.h_mal.farmr.viewmodel import android.os.Bundle import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity import com.appttude.h_mal.farmr.utils.ID import io.mockk.every import io.mockk.mockk @@ -16,7 +17,7 @@ class InfoViewModelTest : ShiftViewModelTest() { fun retrieveData_validBundleAndId_successfulRetrieval() { // Arrange val id = anyLong() - val shift = mockk() + val shift = mockk() val bundle = mockk() // Act @@ -25,7 +26,7 @@ class InfoViewModelTest : ShiftViewModelTest() { viewModel.retrieveData(bundle) // Assert - assertIs(retrieveCurrentData()) + assertIs(retrieveCurrentData()) assertEquals( retrieveCurrentData(), shift @@ -36,7 +37,7 @@ class InfoViewModelTest : ShiftViewModelTest() { fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() { // Arrange val id = anyLong() - val shift = mockk() + val shift = mockk() val bundle = mockk() // Act diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt index 4ba2d24..1a3e6ad 100644 --- a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt @@ -1,17 +1,23 @@ package com.appttude.h_mal.farmr.viewmodel import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.prefs.DATE_IN import com.appttude.h_mal.farmr.data.prefs.DATE_OUT import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ViewState import com.appttude.h_mal.farmr.utils.getOrAwaitValue import com.appttude.h_mal.farmr.utils.getShifts +import io.mockk.MockKAnnotations import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk import org.junit.Assert.assertThrows import org.junit.Before @@ -31,7 +37,10 @@ class MainViewModelTest { @Before fun setUp() { repository = mockk() - every { repository.readShiftsFromDatabase() }.returns(null) + + val mutableLiveData = MutableLiveData>() + val liveData: LiveData> = mutableLiveData + every { repository.readShiftsFromDatabase() }.returns(liveData) every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) viewModel = MainViewModel(repository) } @@ -45,47 +54,60 @@ class MainViewModelTest { @Test fun getShiftsFromRepository_liveDataIsShown() { // Arrange - val listOfShifts = anyList() + val mutableLiveData = MutableLiveData>() + val shifts = anyList() + mutableLiveData.postValue(shifts) + val liveData: LiveData> = mutableLiveData // Act - every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + every { repository.readShiftsFromDatabase() }.returns(liveData) + viewModel = MainViewModel(repository) viewModel.refreshLiveData() // Assert - assertEquals(retrieveCurrentData(), listOfShifts) + assertEquals(retrieveCurrentData(), shifts) } @Test fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() { // Arrange - val listOfShifts = getShifts() + val mutableLiveData = MutableLiveData>() + val shifts = getShifts() + mutableLiveData.postValue(shifts) + val liveData: LiveData> = mutableLiveData // Act - every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + every { repository.readShiftsFromDatabase() }.returns(liveData) + viewModel = MainViewModel(repository) viewModel.refreshLiveData() val retrievedShifts = retrieveCurrentData() val description = viewModel.getInformation() // Assert - assertEquals(retrievedShifts, listOfShifts) + assertEquals(retrievedShifts, shifts.map { it.convertToShiftObject() }) assertEquals( description, "8 Shifts\n" + " (4 Hourly/4 Piece Rate)\n" + - "Total Hours: 4.0\n" + + "Total Hours: 6.0\n" + "Total Units: 4.0\n" + - "Total Pay: £70.00" + "Total Pay: £100.00" ) } @Test fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() { // Arrange - val listOfShifts = getShifts() + val mutableLiveData = MutableLiveData>() + val shifts = getShifts() + mutableLiveData.postValue(shifts) + val liveData: LiveData> = mutableLiveData + val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type } // Act - every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + every { repository.readShiftsFromDatabase() }.returns(liveData) every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type)) + viewModel = MainViewModel(repository) viewModel.refreshLiveData() val retrievedShifts = retrieveCurrentData() val description = viewModel.getInformation() @@ -96,18 +118,18 @@ class MainViewModelTest { val descriptionAfterClearedFilter = viewModel.getInformation() // Assert - assertEquals(retrievedShifts, filteredShifts) + assertEquals(retrievedShifts, filteredShifts.map { it.convertToShiftObject() }) assertEquals( description, "4 Shifts\n" + - "Total Hours: 4.0\n" + - "Total Pay: £30.00" + "Total Hours: 6.0\n" + + "Total Pay: £60.00" ) assertEquals( descriptionAfterClearedFilter, "8 Shifts\n" + " (4 Hourly/4 Piece Rate)\n" + - "Total Hours: 4.0\n" + + "Total Hours: 6.0\n" + "Total Units: 4.0\n" + - "Total Pay: £70.00" + "Total Pay: £100.00" ) }