diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fc9423 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Android Studio +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +.cxx + +# Gradle files +gradle/ +gradlew +gradlew.bat + +# Dependency directories +/node_modules/ + +# Compiled class files +*.class + +# Log files +*.log + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Virtual machine crash logs +hs_err_pid* + +# Android build output +bin/ +out/ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7172fb3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("com.android.application") + id("kotlin-android") + id("kotlin-kapt") + id("dagger.hilt.android.plugin") +} + +dependencies { + // Room Database + implementation("androidx.room:room-runtime:2.5.1") + implementation("androidx.room:room-ktx:2.5.1") + kapt("androidx.room:room-compiler:2.5.1") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") + + // Testing + testImplementation("junit:junit:4.13.2") + testImplementation("io.mockk:mockk:1.13.5") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/src/main/java/com/example/todoapp/data/dao/TodoDao.kt b/src/main/java/com/example/todoapp/data/dao/TodoDao.kt new file mode 100644 index 0000000..f503ab8 --- /dev/null +++ b/src/main/java/com/example/todoapp/data/dao/TodoDao.kt @@ -0,0 +1,17 @@ +package com.example.todoapp.data.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.example.todoapp.data.model.Todo +import kotlinx.coroutines.flow.Flow + +@Dao +interface TodoDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertTodo(todo: Todo): Long + + @Query("SELECT * FROM todos WHERE id = :todoId") + fun getTodoById(todoId: Long): Flow +} \ No newline at end of file diff --git a/src/main/java/com/example/todoapp/data/model/Todo.kt b/src/main/java/com/example/todoapp/data/model/Todo.kt new file mode 100644 index 0000000..fa4569f --- /dev/null +++ b/src/main/java/com/example/todoapp/data/model/Todo.kt @@ -0,0 +1,25 @@ +package com.example.todoapp.data.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.time.LocalDateTime + +@Entity(tableName = "todos") +data class Todo( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val title: String, + val description: String? = null, + val isCompleted: Boolean = false, + val createdAt: LocalDateTime = LocalDateTime.now(), + val updatedAt: LocalDateTime = LocalDateTime.now() +) { + // Validate todo item input + init { + require(title.isNotBlank()) { "Todo title cannot be blank" } + require(title.length <= 100) { "Todo title cannot exceed 100 characters" } + description?.let { + require(it.length <= 500) { "Todo description cannot exceed 500 characters" } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/todoapp/data/repository/TodoRepository.kt b/src/main/java/com/example/todoapp/data/repository/TodoRepository.kt new file mode 100644 index 0000000..f2daf44 --- /dev/null +++ b/src/main/java/com/example/todoapp/data/repository/TodoRepository.kt @@ -0,0 +1,18 @@ +package com.example.todoapp.data.repository + +import com.example.todoapp.data.dao.TodoDao +import com.example.todoapp.data.model.Todo +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class TodoRepository @Inject constructor( + private val todoDao: TodoDao +) { + suspend fun addTodo(todo: Todo): Long { + return todoDao.insertTodo(todo) + } + + fun getTodoById(todoId: Long): Flow { + return todoDao.getTodoById(todoId) + } +} \ No newline at end of file diff --git a/src/test/java/com/example/todoapp/data/repository/TodoRepositoryTest.kt b/src/test/java/com/example/todoapp/data/repository/TodoRepositoryTest.kt new file mode 100644 index 0000000..08e1899 --- /dev/null +++ b/src/test/java/com/example/todoapp/data/repository/TodoRepositoryTest.kt @@ -0,0 +1,47 @@ +package com.example.todoapp.data.repository + +import com.example.todoapp.data.dao.TodoDao +import com.example.todoapp.data.model.Todo +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class TodoRepositoryTest { + private val todoDao = mockk() + private val todoRepository = TodoRepository(todoDao) + + @Test + fun `addTodo should save todo item successfully`() = runBlocking { + // Arrange + val todo = Todo(title = "Test Todo") + coEvery { todoDao.insertTodo(todo) } returns 1L + + // Act + val result = todoRepository.addTodo(todo) + + // Assert + assertEquals(1L, result) + } + + @Test(expected = IllegalArgumentException::class) + fun `addTodo should throw exception for blank title`() = runBlocking { + // Arrange + val todo = Todo(title = "") + + // Act & Assert + todoRepository.addTodo(todo) + } + + @Test(expected = IllegalArgumentException::class) + fun `addTodo should throw exception for title exceeding max length`() = runBlocking { + // Arrange + val longTitle = "a".repeat(101) + val todo = Todo(title = longTitle) + + // Act & Assert + todoRepository.addTodo(todo) + } +} \ No newline at end of file