Repository: abu-dhabi-squad/Plan-Mate
Pull request #9: implement task validator with its helper classes (date validator , da…
Author: login: Noor-Serry
Created: 2025-04-29T00:34:49Z | Merged: 2025-04-29T14:37:51Z

…te parser) with unit test with test coverage 100 %


Diff:
diff --git a/src/main/kotlin/logic/exceptions/Exceptions.kt b/src/main/kotlin/logic/exceptions/Exceptions.kt
index 38184a22..5ff64f82 100644
--- a/src/main/kotlin/logic/exceptions/Exceptions.kt
+++ b/src/main/kotlin/logic/exceptions/Exceptions.kt
@@ -7,4 +7,8 @@ class IllegalTaskException(error: String = "Invalid task data") : Exception(erro
 class InvalidDateException(error: String = "Invalid date") : Exception(error)
 
 class TaskNotFoundException(error: String = "Task not found") : Exception(error)
-class NoTasksFoundException(error: String = "No tasks found" ) : Exception(error)
\ No newline at end of file
+class NoTasksFoundException(error: String = "No tasks found" ) : Exception(error)
+
+class InvalidDateFormatException(error: String = "Invalid date format") : Exception(error)
+class InvalidYearException(error: String = "Invalid year") : Exception(error)
+class InvalidTaskDateException(error: String = "Invalid task date") : Exception(error)
\ No newline at end of file
diff --git a/src/main/kotlin/logic/validation/DateParser.kt b/src/main/kotlin/logic/validation/DateParser.kt
new file mode 100644
index 00000000..8f2043e0
--- /dev/null
+++ b/src/main/kotlin/logic/validation/DateParser.kt
@@ -0,0 +1,7 @@
+package squad.abudhabi.logic.validation
+
+import java.time.LocalDate
+
+interface DateParser {
+    fun parseDateFromString(date: String): LocalDate
+}
\ No newline at end of file
diff --git a/src/main/kotlin/logic/validation/DateParserImpl.kt b/src/main/kotlin/logic/validation/DateParserImpl.kt
new file mode 100644
index 00000000..89a853a9
--- /dev/null
+++ b/src/main/kotlin/logic/validation/DateParserImpl.kt
@@ -0,0 +1,11 @@
+package squad.abudhabi.logic.validation
+
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+
+class DateParserImpl: DateParser {
+    override fun parseDateFromString(date: String): LocalDate {
+        val formatter = DateTimeFormatter.ofPattern("yyyy-M-d")
+        return LocalDate.parse(date, formatter)
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/logic/validation/DateValidator.kt b/src/main/kotlin/logic/validation/DateValidator.kt
index 46a1a6fc..28f68cef 100644
--- a/src/main/kotlin/logic/validation/DateValidator.kt
+++ b/src/main/kotlin/logic/validation/DateValidator.kt
@@ -1,4 +1,5 @@
 package squad.abudhabi.logic.validation
 
 interface DateValidator {
+    fun validateDateOrThrow(date: String)
 }
\ No newline at end of file
diff --git a/src/main/kotlin/logic/validation/DateValidatorImpl.kt b/src/main/kotlin/logic/validation/DateValidatorImpl.kt
new file mode 100644
index 00000000..a535693c
--- /dev/null
+++ b/src/main/kotlin/logic/validation/DateValidatorImpl.kt
@@ -0,0 +1,26 @@
+package squad.abudhabi.logic.validation
+
+import squad.abudhabi.logic.exceptions.InvalidDateFormatException
+import squad.abudhabi.logic.exceptions.InvalidYearException
+import java.time.LocalDate
+
+class DateValidatorImpl(
+    private val dateParserInterface: DateParser
+) : DateValidator {
+
+    override fun validateDateOrThrow(date: String) {
+        val formatedDate: LocalDate
+        try {
+            formatedDate = dateParserInterface.parseDateFromString(date)
+        } catch (e: Exception) {
+            throw InvalidDateFormatException()
+        }
+
+        validateYearOrThrow(formatedDate.year)
+    }
+
+    private fun validateYearOrThrow(year: Int) {
+        if (year < 2000) throw InvalidYearException()
+    }
+
+}
diff --git a/src/main/kotlin/logic/validation/TaskValidatorImpl.kt b/src/main/kotlin/logic/validation/TaskValidatorImpl.kt
new file mode 100644
index 00000000..ab8e63f1
--- /dev/null
+++ b/src/main/kotlin/logic/validation/TaskValidatorImpl.kt
@@ -0,0 +1,22 @@
+package squad.abudhabi.logic.validation
+
+import squad.abudhabi.logic.exceptions.InvalidTaskDateException
+
+
+class TaskValidatorImpl(
+    private val dateValidator: DateValidator,
+    private val dateParser: DateParser
+) : TaskValidator {
+
+    override fun validateOrThrow(startDate: String, endDate: String) {
+        try {
+            dateValidator.validateDateOrThrow(startDate)
+            dateValidator.validateDateOrThrow(endDate)
+            val start = dateParser.parseDateFromString(startDate)
+            val end = dateParser.parseDateFromString(endDate)
+            if (end.isBefore(start)) throw InvalidTaskDateException("End date cannot be before start date")
+        } catch (ex: Exception) {
+            throw InvalidTaskDateException("" + ex.message)
+        }
+    }
+}
diff --git a/src/test/kotlin/logic/validation/DateParserImplTest.kt b/src/test/kotlin/logic/validation/DateParserImplTest.kt
new file mode 100644
index 00000000..337c3263
--- /dev/null
+++ b/src/test/kotlin/logic/validation/DateParserImplTest.kt
@@ -0,0 +1,76 @@
+package logic.validation
+
+import com.google.common.truth.Truth
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.CsvSource
+import org.junit.jupiter.params.provider.ValueSource
+import squad.abudhabi.logic.validation.DateParserImpl
+import java.time.LocalDate
+import java.time.format.DateTimeParseException
+import kotlin.test.assertFailsWith
+
+class DateParserImplTest {
+
+    private lateinit var dateParserImpl: DateParserImpl
+
+    @BeforeEach
+    fun setup() {
+        dateParserImpl = DateParserImpl()
+    }
+
+    @ParameterizedTest
+    @CsvSource("2025,4,30", "2025,1,1", "2025,12,31")
+    fun `parseDateFromString should return object of local date when the input is valid`(
+        year: Int,
+        month: Int,
+        day: Int
+    ) {
+        //given
+        val date = "$year-$month-$day"
+        //when
+        val res = dateParserImpl.parseDateFromString(date)
+        //then
+        Truth.assertThat(res).isEqualTo(LocalDate.of(year, month, day))
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = [
+        "2020-10-0",
+        "2020-10-32",
+        "2020-13-10",
+        "2020-0-10"
+    ])
+    fun `should throw DateTimeParseException when the input contains invalid day or month`(date: String) {
+        // when & then
+        assertFailsWith<DateTimeParseException> {
+            dateParserImpl.parseDateFromString(date)
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = [
+        "2025/4/30",
+        "2025&4&30",
+        "2025a4a30",
+        "2025.4.30"
+    ])
+    fun `should throw DateTimeParseException when the input contains invalid character`(date: String) {
+        // when & then
+        assertFailsWith<DateTimeParseException> {
+            dateParserImpl.parseDateFromString(date)
+        }
+    }
+
+
+    @Test
+    fun `parseDateFromString should return date of the last day in month when the input' day is more than the last day of the month`() {
+        //given
+        val date = "2025-4-31"
+        val res = LocalDate.of(2025, 4, 30)
+        //when & then
+        Truth.assertThat(dateParserImpl.parseDateFromString(date)).isEqualTo(res)
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/kotlin/logic/validation/DateValidatorImplTest.kt b/src/test/kotlin/logic/validation/DateValidatorImplTest.kt
new file mode 100644
index 00000000..38d476d2
--- /dev/null
+++ b/src/test/kotlin/logic/validation/DateValidatorImplTest.kt
@@ -0,0 +1,64 @@
+package logic.validation
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import squad.abudhabi.logic.exceptions.InvalidDateFormatException
+import squad.abudhabi.logic.exceptions.InvalidYearException
+import squad.abudhabi.logic.validation.DateParser
+import squad.abudhabi.logic.validation.DateValidatorImpl
+import java.time.LocalDate
+import kotlin.test.Test
+
+class DateValidatorImplTest {
+    private lateinit var dateParserInterface: DateParser
+    private lateinit var dateValidatorImpl: DateValidatorImpl
+
+    @BeforeEach
+    fun setup() {
+        dateParserInterface = mockk(relaxed = true)
+        dateValidatorImpl = DateValidatorImpl(dateParserInterface)
+    }
+
+    @Test
+    fun `should not throw exception when date is valid`() {
+        //given
+        val date = "2020-2-10"
+        every { dateParserInterface.parseDateFromString(date) } returns LocalDate.of(2020, 2, 10)
+        //when
+        dateValidatorImpl.validateDateOrThrow(date)
+        //then
+        verify(exactly = 1) { dateParserInterface.parseDateFromString(date) }
+    }
+
+    @Test
+    fun `should throw InvalidDateFormatException when invalid date format provided`() {
+        //given
+        every { dateParserInterface.parseDateFromString(any()) } throws InvalidDateFormatException()
+        //when
+        assertThrows<InvalidDateFormatException> {
+            dateValidatorImpl.validateDateOrThrow("2000/10-10")
+        }
+    }
+    @ParameterizedTest
+    @ValueSource(ints = [1999, 1900, 1000, -1000])
+    fun `should throw InvalidYearException when year from the is very old`(year:Int) {
+        //given
+        val invalidOldYear = year
+        val date = "$invalidOldYear-10-10"
+        every { dateParserInterface.parseDateFromString(date) } returns LocalDate.of(
+            invalidOldYear,
+            10,
+            10
+        )
+        //when & then
+        assertThrows<InvalidYearException> {
+            dateValidatorImpl.validateDateOrThrow(date)
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/kotlin/logic/validation/TaskValidatorImplTest.kt b/src/test/kotlin/logic/validation/TaskValidatorImplTest.kt
new file mode 100644
index 00000000..b02f66cd
--- /dev/null
+++ b/src/test/kotlin/logic/validation/TaskValidatorImplTest.kt
@@ -0,0 +1,69 @@
+package logic.validation
+
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.CsvSource
+import squad.abudhabi.logic.exceptions.InvalidDateException
+import squad.abudhabi.logic.exceptions.InvalidTaskDateException
+import squad.abudhabi.logic.validation.DateParser
+import squad.abudhabi.logic.validation.DateValidator
+import squad.abudhabi.logic.validation.TaskValidatorImpl
+import java.time.LocalDate
+
+class TaskValidatorImplTest {
+    private lateinit var taskValidator: TaskValidatorImpl
+    private lateinit var dateValidator: DateValidator
+    private lateinit var dateParser: DateParser
+    @BeforeEach
+    fun setup() {
+        dateValidator = mockk(relaxed = true)
+        dateParser = mockk(relaxed = true)
+        taskValidator = TaskValidatorImpl(dateValidator, dateParser)
+    }
+
+    @Test
+    fun `should not throw exception when task data is valid`() {
+        // given
+        val startData = "2025-04-01"
+        val endDate = "2025-05-01"
+        // when && then
+        taskValidator.validateOrThrow(startData, endDate)
+    }
+
+    @ParameterizedTest
+    @CsvSource(
+        "'',2025-05-01",
+        "01-04-2025,2025-05-01",
+        "'',''"
+    )
+    fun `should throw InvalidDateException when task data is invalid`(
+        startData: String,
+        endDate: String
+    ) {
+        every { dateValidator.validateDateOrThrow(startData)} throws InvalidDateException()
+        every { dateValidator.validateDateOrThrow(endDate)} throws InvalidDateException()
+        every { dateParser.parseDateFromString(startData)} throws InvalidDateException()
+        every { dateParser.parseDateFromString(endDate)} throws InvalidDateException()
+        // when && then
+        assertThrows<InvalidTaskDateException> {
+            taskValidator.validateOrThrow(startData, endDate)
+        }
+    }
+
+    @Test
+    fun `should throw InvalidTaskDateException when end date is before start date`(){
+        // given
+        val startData = "2025-05-01"
+        val endDate = "2025-04-01"
+        every { dateParser.parseDateFromString(startData)} returns  LocalDate.parse(startData)
+        every { dateParser.parseDateFromString(endDate)} returns  LocalDate.parse(endDate)
+        // when
+        assertThrows<InvalidTaskDateException> {
+            taskValidator.validateOrThrow(startData, endDate)
+        }
+    }
+}
\ No newline at end of file


Reviews:
user: login: eslam-99
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:18:39Z
user: login: Noor-Serry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:36:30Z

Review comments:
diff_hunk: @@ -0,0 +1,22 @@
+package squad.abudhabi.logic.validation
+
+import squad.abudhabi.logic.exceptions.InvalidTaskDateException
+
+
+class TaskValidatorImpl(
+    private val dateValidator: DateValidator,
+    private val dateParser: DateParser
+) : TaskValidator {
+
+    override fun validateOrThrow(startDate: String, endDate: String) {
+        try {
+            dateValidator.validateDateOrThrow(startDate)
+            dateValidator.validateDateOrThrow(endDate)
+            val start = dateParser.parseDateFromString(startDate)
+            val end = dateParser.parseDateFromString(endDate)
+            if (end.isBefore(start)) throw InvalidTaskDateException("End date cannot be before start date")
path: src/main/kotlin/logic/validation/TaskValidatorImpl.kt
user: login: eslam-99
body: i think it's better to have exception class with name like EndBeforeStartDateException istead of passing custom msg to exception 
created_at: 2025-04-29T13:18:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+package squad.abudhabi.logic.validation
+
+import squad.abudhabi.logic.exceptions.InvalidTaskDateException
+
+
+class TaskValidatorImpl(
+    private val dateValidator: DateValidator,
+    private val dateParser: DateParser
+) : TaskValidator {
+
+    override fun validateOrThrow(startDate: String, endDate: String) {
+        try {
+            dateValidator.validateDateOrThrow(startDate)
+            dateValidator.validateDateOrThrow(endDate)
+            val start = dateParser.parseDateFromString(startDate)
+            val end = dateParser.parseDateFromString(endDate)
+            if (end.isBefore(start)) throw InvalidTaskDateException("End date cannot be before start date")
path: src/main/kotlin/logic/validation/TaskValidatorImpl.kt
user: login: Noor-Serry
body: The name of the class is Task validator so we validated the task maybe in future we add other parameter to this function to be validate 
created_at: 2025-04-29T13:36:30Z
author_association: CONTRIBUTOR

PR comments: