From 7b14c5823078393e2c2e2924578357401573a17c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 29 Aug 2026 01:32:47 +0200 Subject: [PATCH 1/3] feat: mark fields with nullable type as Optional --- .../de/thetadev/formconductor/utils/ReflectionExtensions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt b/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt index 4142709..0f79574 100644 --- a/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt +++ b/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt @@ -13,7 +13,8 @@ inline fun KProperty1.isAnnotatedWith(annota return this.hasAnnotation() } -fun KProperty1.isFieldOptional() : Boolean = hasAnnotation() +fun KProperty1.isFieldOptional(): Boolean = + hasAnnotation() || returnType.isMarkedNullable fun KProperty1.validationAnnotations() = annotations.filter(Annotation::isFieldValidation) From 1bca38e6320def3783e0dd056f5d646c1f2bc4c4 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 29 Aug 2026 01:33:08 +0200 Subject: [PATCH 2/3] test: add unit tests --- core/build.gradle.kts | 9 ++ .../formconductor/builder/FormBuilderTest.kt | 100 +++++++++++++++++ .../form/FormImplBehaviorTest.kt | 101 ++++++++++++++++++ .../formconductor/form/FormResultTest.kt | 45 ++++++++ .../formconductor/form/MockedForms.kt | 19 ++++ .../thetadev/formconductor/form/TestForm.kt | 9 ++ .../validation/EmailAddressRuleTest.kt | 63 +++++++++++ .../validation/FloatRangeRuleTest.kt | 44 ++++++++ .../validation/IntegerRangeRuleTest.kt | 37 +++++++ .../validation/IsCheckedRuleTest.kt | 25 +++++ .../validation/MaxLengthRuleTest.kt | 27 +++++ .../validation/MinLengthRuleTest.kt | 28 +++++ .../validation/WebUrlRuleTest.kt | 43 ++++++++ 13 files changed, 550 insertions(+) create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/builder/FormBuilderTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/form/FormImplBehaviorTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/form/FormResultTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/form/MockedForms.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/form/TestForm.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/validation/EmailAddressRuleTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/validation/FloatRangeRuleTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/validation/IntegerRangeRuleTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/validation/IsCheckedRuleTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/validation/MaxLengthRuleTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/validation/MinLengthRuleTest.kt create mode 100644 core/src/test/kotlin/de/thetadev/formconductor/validation/WebUrlRuleTest.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 0f93175..21b0d53 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -12,10 +12,19 @@ kotlin { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 } } +tasks.test { + useJUnitPlatform() +} dependencies { implementation(libs.kotlin.reflect) implementation(libs.kotlinx.coroutines.core) + + testImplementation(platform("org.junit:junit-bom:5.14.4")) + testImplementation("org.junit.jupiter:junit-jupiter:5.14.4") + testImplementation("org.amshove.kluent:kluent:1.71") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } apply(from = "$rootDir/scripts/publish-root.gradle.kts") diff --git a/core/src/test/kotlin/de/thetadev/formconductor/builder/FormBuilderTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/builder/FormBuilderTest.kt new file mode 100644 index 0000000..2790352 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/builder/FormBuilderTest.kt @@ -0,0 +1,100 @@ +package de.thetadev.formconductor.builder + +import de.thetadev.formconductor.FieldResult +import de.thetadev.formconductor.FormField +import de.thetadev.formconductor.FormFieldImpl +import de.thetadev.formconductor.FormImpl +import de.thetadev.formconductor.annotations.Form +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.should +import org.amshove.kluent.`should be` +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.`should be instance of` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class FormBuilderTest { + @Form + data class MockForm( + val arg1: String = "" + ) + + @Test + fun `form builder function returns correct instance`() { + val form = form(MockForm::class) + form `should be instance of` FormImpl::class + form `should be instance of` de.thetadev.formconductor.Form::class + } + + @Test + fun `form builder function returns correct builder scope`() = runTest { + form(MockForm::class) { + this `should be instance of` FormBuilderScopeImpl::class + this `should be instance of` FormBuilderScope::class + this.formState `should be instance of` Flow::class + } + } + + @Test + fun `form builder scope functions properly`() = runTest { + form(MockForm::class) { + this.registerField(MockForm::arg1) `should be instance of` FormFieldImpl::class + this.registerField(MockForm::arg1) `should be instance of` FormField::class + } + } + + @Test + fun `field builder function returns correct result`() = runTest { + form(MockForm::class) { + val field = field(MockForm::arg1) + field `should be instance of` FormField::class + field.fieldName.trim() `should be equal to` MockForm::arg1.name.trim() + field.isFieldOptional() `should be` false + field.resultStream `should be instance of` MutableStateFlow::class + field.valueStream `should be instance of` MutableStateFlow::class + field.value should { + this == "" || this == null + } + } + } + + @Test + fun `field builder scope functions properly`() = runTest { + form(MockForm::class) { + field(MockForm::arg1) { + this.setField("Test") + this.resultState.value `should be equal to` FieldResult.Success + this.state.value `should be equal to` "Test" + } + } + } + + @Test + fun `field builder extension functions returns correct result`() = runTest { + val form = form(MockForm::class) {} + val field = form.field(MockForm::arg1) + field `should be instance of` FormField::class + field.fieldName.trim() `should be equal to` MockForm::arg1.name.trim() + field.isFieldOptional() `should be` false + field.resultStream `should be instance of` MutableStateFlow::class + field.valueStream `should be instance of` MutableStateFlow::class + field.value should { + this == "" || this == null + } + } + + @Test + fun `field builder extension scope functions properly`() = runTest { + val form = form(MockForm::class) {} + form.field(MockForm::arg1) { + this.setField("Test") + this.resultState.value `should be equal to` FieldResult.Success + this.state.value `should be equal to` "Test" + } + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/form/FormImplBehaviorTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/form/FormImplBehaviorTest.kt new file mode 100644 index 0000000..741bc33 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/form/FormImplBehaviorTest.kt @@ -0,0 +1,101 @@ +package de.thetadev.formconductor.form + +import de.thetadev.formconductor.FormField +import de.thetadev.formconductor.FormImpl +import de.thetadev.formconductor.FormResult +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.should +import org.amshove.kluent.`should be` +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.`should be instance of` +import org.amshove.kluent.`should not be` +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class FormImplBehaviorTest { + + private lateinit var form: FormImpl + + @BeforeEach + fun setUp() { + form = FormImpl(TestForm::class) + } + + + @Nested + inner class DataStreams { + + @Test + fun `form data stream emits a non-null value as first item`() = runTest { + form.formDataStream.firstOrNull() `should not be` null + } + + @Test + fun `form data stream emits NoInput as first item`() = runTest { + val firstEmittedItem = form.formDataStream.firstOrNull() + firstEmittedItem `should be` FormResult.NoInput + } + + } + + + @Nested + inner class FormConstruction { + /* + * Type checks + */ + @Test + fun `form construction should fail on field type and annotation mismatch`() { + assertThrows { + FormImpl(MockedForms.AnnotationPropertyTypeMismatch::class) + } + } + + @Test + fun `form construction should fail on field type and annotation multiple mismatches`() { + assertThrows { + FormImpl(MockedForms.AnnotationPropertyTypeMismatch2::class) + } + } + } + + @Nested + inner class FieldHandling { + @Test + fun `registerField returns correct field`() { + val field = form.registerField(TestForm::integerField) + + field `should be instance of` FormField::class + field.fieldName.trim() `should be equal to` TestForm::integerField.name.trim() + field.isFieldOptional() `should be` false + field.resultStream `should be instance of` MutableStateFlow::class + field.valueStream `should be instance of` MutableStateFlow::class + field.value should { + this == 0 || this == null + } + } + + @Test + fun `registerField returns correct field 2`() { + val field = form.registerField(TestForm::stringField) + + field `should be instance of` FormField::class + field.fieldName.trim() `should be equal to` TestForm::stringField.name.trim() + field.isFieldOptional() `should be` false + field.resultStream `should be instance of` MutableStateFlow::class + field.valueStream `should be instance of` MutableStateFlow::class + field.value should { + this == "" || this == null + } + } + } + +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/form/FormResultTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/form/FormResultTest.kt new file mode 100644 index 0000000..83a6c22 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/form/FormResultTest.kt @@ -0,0 +1,45 @@ +package de.thetadev.formconductor.form + +import de.thetadev.formconductor.FormImpl +import de.thetadev.formconductor.FormResult +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.`should be instance of` +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class FormResultTest { + + private lateinit var form: FormImpl + + @BeforeEach + fun setUp() { + form = FormImpl(TestForm::class) + } + + @Test + fun `submit returns correct result for no input`() { + form.validate() `should be instance of` FormResult.Error::class + } + + @Test + fun `submit returns success result for valid input`() = runTest { + val expectedResult = FormResult.Valid(TestForm(stringField = "Test", integerField = 10)) + form.setField(TestForm::stringField, "Test") + form.setField(TestForm::integerField, 10) + val result = form.validate() + result `should be equal to` expectedResult + } + + @Test + fun `submit returns error result for invalid input`() = runTest { + form.setField(TestForm::stringField, "Test") + form.setField(TestForm::integerField, 9) + val result = form.validate() + result `should be instance of` FormResult.Error::class + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/form/MockedForms.kt b/core/src/test/kotlin/de/thetadev/formconductor/form/MockedForms.kt new file mode 100644 index 0000000..56be062 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/form/MockedForms.kt @@ -0,0 +1,19 @@ +package de.thetadev.formconductor.form + +import de.thetadev.formconductor.annotations.EmailAddress +import de.thetadev.formconductor.annotations.IntegerRange + +object MockedForms { + data class AnnotationPropertyTypeMismatch( + @IntegerRange(0, 1) + val arg1: String = "" + ) + + data class AnnotationPropertyTypeMismatch2( + val arg1: String = "", + @EmailAddress + val arg2: Int = 0, + @IntegerRange(0, 1) + val arg3: String = "" + ) +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/form/TestForm.kt b/core/src/test/kotlin/de/thetadev/formconductor/form/TestForm.kt new file mode 100644 index 0000000..7a6a784 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/form/TestForm.kt @@ -0,0 +1,9 @@ +package de.thetadev.formconductor.form + +import de.thetadev.formconductor.annotations.IntegerRange + +data class TestForm( + val stringField: String = "", + @IntegerRange(min = 10, max = 100) + val integerField: Int = 0 +) diff --git a/core/src/test/kotlin/de/thetadev/formconductor/validation/EmailAddressRuleTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/validation/EmailAddressRuleTest.kt new file mode 100644 index 0000000..b7a7f5e --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/validation/EmailAddressRuleTest.kt @@ -0,0 +1,63 @@ +package de.thetadev.formconductor.validation + +import de.thetadev.formconductor.FormImpl +import de.thetadev.formconductor.FormResult +import de.thetadev.formconductor.annotations.EmailAddress +import de.thetadev.formconductor.annotations.Form +import de.thetadev.formconductor.annotations.OptionalField +import de.thetadev.formconductor.validation.rules.EmailAddressRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class EmailAddressRuleTest { + + @Form + data class MockForm( + @EmailAddress + val email: String = "", + @EmailAddress + @OptionalField + val emailOpt: String? = null + ) + + private lateinit var form: FormImpl + + @BeforeEach + fun setUp() { + form = FormImpl(MockForm::class) + } + + @Test + fun `rule yields correct success result for annotated validations`() = runTest { + val formData = MockForm(email = "hello@example.com") + form.setValues(formData) + form.validate() `should be equal to` FormResult.Valid(formData) + } + + @Test + fun `rule yields correct success result for both annotated validations`() = runTest { + val formData = MockForm(email = "hello@example.com", emailOpt = "bye@example.com") + form.setValues(formData) + form.validate() `should be equal to` FormResult.Valid(formData) + } + + @Test + fun `rule yields correct error result for annotated validations`() = runTest { + val formData = MockForm(email = "hello") + form.setValues(formData) + form.validate() `should be equal to` FormResult.Error(setOf(EmailAddressRule)) + } + + @Test + fun `rule yields correct error result for opt annotated validation`() = runTest { + val formData = MockForm(email = "hello@example.com", emailOpt = "bye") + form.setValues(formData) + form.validate() `should be equal to` FormResult.Error(setOf(EmailAddressRule)) + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/validation/FloatRangeRuleTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/validation/FloatRangeRuleTest.kt new file mode 100644 index 0000000..3462fc5 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/validation/FloatRangeRuleTest.kt @@ -0,0 +1,44 @@ +package de.thetadev.formconductor.validation + +import de.thetadev.formconductor.FieldResult +import de.thetadev.formconductor.annotations.FloatRange +import de.thetadev.formconductor.validation.rules.FloatRangeRule +import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class FloatRangeRuleTest { + + @Test + fun `rule return success for correct values`() { + val options = FloatRange(min = 0.0, max = 1.0) + + FloatRangeRule.validate(0.0f, options) `should be equal to` FieldResult.Success + + FloatRangeRule.validate(0.1f, options) `should be equal to` FieldResult.Success + + FloatRangeRule.validate(1.0f, options) `should be equal to` FieldResult.Success + } + + @Test + fun `rule return error for invalid values`() { + val options = FloatRange(min = 0.0, max = 1.0) + + FloatRangeRule.validate( + -0.1f, + options + ) `should be equal to` FieldResult.Error("Value out of range", FloatRangeRule) + + FloatRangeRule.validate( + 10f, + options + ) `should be equal to` FieldResult.Error("Value out of range", FloatRangeRule) + + FloatRangeRule.validate( + 1.001f, + options + ) `should be equal to` FieldResult.Error("Value out of range", FloatRangeRule) + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/validation/IntegerRangeRuleTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/validation/IntegerRangeRuleTest.kt new file mode 100644 index 0000000..153fa7c --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/validation/IntegerRangeRuleTest.kt @@ -0,0 +1,37 @@ +package de.thetadev.formconductor.validation + +import de.thetadev.formconductor.FieldResult +import de.thetadev.formconductor.annotations.IntegerRange +import de.thetadev.formconductor.validation.rules.IntegerRangeRule +import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class IntegerRangeRuleTest { + @Test + fun `rule return success for correct values`() { + val options = IntegerRange(min = 0, max = 10) + + IntegerRangeRule.validate(0, options) `should be equal to` FieldResult.Success + + IntegerRangeRule.validate(5, options) `should be equal to` FieldResult.Success + + IntegerRangeRule.validate(10, options) `should be equal to` FieldResult.Success + } + + @Test + fun `rule return error for invalid values`() { + val options = IntegerRange(min = 0, max = 10) + + IntegerRangeRule.validate( + -1, + options + ) `should be equal to` FieldResult.Error("Value out of range", IntegerRangeRule) + + IntegerRangeRule.validate( + 11, + options + ) `should be equal to` FieldResult.Error("Value out of range", IntegerRangeRule) + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/validation/IsCheckedRuleTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/validation/IsCheckedRuleTest.kt new file mode 100644 index 0000000..3af5f8a --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/validation/IsCheckedRuleTest.kt @@ -0,0 +1,25 @@ +package de.thetadev.formconductor.validation + +import de.thetadev.formconductor.FieldResult +import de.thetadev.formconductor.annotations.IsChecked +import de.thetadev.formconductor.validation.rules.IsCheckedRule +import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class IsCheckedRuleTest { + private val options = IsChecked() + + @Test + fun `rule returns success for correct input`() { + IsCheckedRule.validate(true, options) `should be equal to` FieldResult.Success + } + + @Test + fun `rule returns error for invalid input`() { + val expectedResult = + FieldResult.Error("This field is required to be checked", IsCheckedRule) + IsCheckedRule.validate(false, options) `should be equal to` expectedResult + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/validation/MaxLengthRuleTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/validation/MaxLengthRuleTest.kt new file mode 100644 index 0000000..b9f8394 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/validation/MaxLengthRuleTest.kt @@ -0,0 +1,27 @@ +package de.thetadev.formconductor.validation + +import de.thetadev.formconductor.FieldResult +import de.thetadev.formconductor.annotations.MaxLength +import de.thetadev.formconductor.validation.rules.MaxLengthRule +import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class MaxLengthRuleTest { + @Test + fun `rule returns success for correct input`() { + val options = MaxLength(5) + MaxLengthRule.validate("12345", options) `should be equal to` FieldResult.Success + MaxLengthRule.validate("1234", options) `should be equal to` FieldResult.Success + MaxLengthRule.validate("1", options) `should be equal to` FieldResult.Success + } + + @Test + fun `rule returns error for invalid input`() { + val options = MaxLength(5) + val expectedResult = + FieldResult.Error("Value shouldn't be longer than ${options.value}.", MaxLengthRule) + MaxLengthRule.validate("123456", options) `should be equal to` expectedResult + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/validation/MinLengthRuleTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/validation/MinLengthRuleTest.kt new file mode 100644 index 0000000..7a21a27 --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/validation/MinLengthRuleTest.kt @@ -0,0 +1,28 @@ +package de.thetadev.formconductor.validation + +import de.thetadev.formconductor.FieldResult +import de.thetadev.formconductor.annotations.MinLength +import de.thetadev.formconductor.validation.rules.MinLengthRule +import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class MinLengthRuleTest { + @Test + fun `rule returns success for correct input`() { + val options = MinLength(5) + MinLengthRule.validate("123456", options) `should be equal to` FieldResult.Success + MinLengthRule.validate("12345", options) `should be equal to` FieldResult.Success + } + + @Test + fun `rule returns error for invalid input`() { + val options = MinLength(5) + val expectedResult = + FieldResult.Error("Value shouldn't be shorter than ${options.value}.", MinLengthRule) + + MinLengthRule.validate("1234", options) `should be equal to` expectedResult + MinLengthRule.validate("1", options) `should be equal to` expectedResult + } +} diff --git a/core/src/test/kotlin/de/thetadev/formconductor/validation/WebUrlRuleTest.kt b/core/src/test/kotlin/de/thetadev/formconductor/validation/WebUrlRuleTest.kt new file mode 100644 index 0000000..6360b5c --- /dev/null +++ b/core/src/test/kotlin/de/thetadev/formconductor/validation/WebUrlRuleTest.kt @@ -0,0 +1,43 @@ +package de.thetadev.formconductor.validation + +import de.thetadev.formconductor.FieldResult +import de.thetadev.formconductor.annotations.WebUrl +import de.thetadev.formconductor.validation.rules.WebUrlRule +import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class WebUrlRuleTest { + @Test + fun `rule returns success result for correct url input`() { + val options = WebUrl() + WebUrlRule.validate("https://google.com", options) `should be equal to` FieldResult.Success + WebUrlRule.validate("www.google.com", options) `should be equal to` FieldResult.Success + WebUrlRule.validate("google.com", options) `should be equal to` FieldResult.Success + } + + @Test + fun `rule returns error for invalid url input`() { + val options = WebUrl() + val expectedResult = FieldResult.Error("Invalid URL", WebUrlRule) + WebUrlRule.validate("https://abc", options) `should be equal to` expectedResult + WebUrlRule.validate("random", options) `should be equal to` expectedResult + } + + @Test + fun `rule returns success result for correct https url input`() { + val options = WebUrl(httpRequired = true) + WebUrlRule.validate("https://google.com", options) `should be equal to` FieldResult.Success + WebUrlRule.validate("http://google.com", options) `should be equal to` FieldResult.Success + } + + @Test + fun `rule returns error for invalid https url input`() { + val options = WebUrl(true) + val expectedResult = FieldResult.Error("Invalid URL", WebUrlRule) + WebUrlRule.validate("https://abc", options) `should be equal to` expectedResult + WebUrlRule.validate("random", options) `should be equal to` expectedResult + WebUrlRule.validate("google.com", options) `should be equal to` expectedResult + } +} From 56a286bf1f61d4ab09a9e6e0b4467ee26a3c5be4 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 29 Aug 2026 01:35:57 +0200 Subject: [PATCH 3/3] Revert "feat: mark fields with nullable type as Optional" This reverts commit 7b14c5823078393e2c2e2924578357401573a17c. --- .../de/thetadev/formconductor/utils/ReflectionExtensions.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt b/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt index 0f79574..4142709 100644 --- a/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt +++ b/core/src/main/java/de/thetadev/formconductor/utils/ReflectionExtensions.kt @@ -13,8 +13,7 @@ inline fun KProperty1.isAnnotatedWith(annota return this.hasAnnotation() } -fun KProperty1.isFieldOptional(): Boolean = - hasAnnotation() || returnType.isMarkedNullable +fun KProperty1.isFieldOptional() : Boolean = hasAnnotation() fun KProperty1.validationAnnotations() = annotations.filter(Annotation::isFieldValidation)