Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
namespace = "com.pingidentity.${project.name.replace("-", ".")}"
// bcprov-jdk18on 1.83+ contains Java 25 multi-release class files (major version 69).
// AGP's default JaCoCo 0.8.12 (ASM 9.7) cannot instrument them; 0.8.13 (ASM 9.8) can.
testCoverage { jacocoVersion = "0.8.13" }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #201 broke the pipeline. This fixes it...

publishing {
singleVariant("release") {
withSourcesJar()
Expand Down
1 change: 1 addition & 0 deletions davinci/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ dependencies {
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(project(":foundation:testrail"))
androidTestImplementation(project(":protect"))
androidTestImplementation("com.google.zxing:core:3.5.3")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZXing dependency not version-catalogued

androidTestImplementation("com.google.zxing:core:3.5.3")

Every other dependency in this file uses the libs.* version catalog alias (e.g. libs.androidx.test.runner). Adding a hard-coded GAV string bypasses the catalog, making it invisible to dependencyUpdates checks and inconsistent with the project style.

Add zxing-core = { module = "com.google.zxing:core", version = "3.5.3" } to gradle/libs.versions.toml and reference it as libs.zxing.core here.

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.test.filters.SmallTest
import com.pingidentity.davinci.collector.FlowCollector
import com.pingidentity.davinci.collector.InvalidLength
import com.pingidentity.davinci.collector.Length
import com.pingidentity.davinci.collector.MaxRepeat
import com.pingidentity.davinci.collector.MinCharacters
import com.pingidentity.davinci.collector.PasswordCollector
import com.pingidentity.davinci.collector.RegexError
Expand All @@ -28,14 +29,17 @@ import com.pingidentity.orchestrate.ContinueNode
import com.pingidentity.orchestrate.ErrorNode
import com.pingidentity.testrail.TestRailCase
import com.pingidentity.testrail.TestRailWatcher
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.rules.TestWatcher
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@SmallTest
Expand All @@ -44,11 +48,11 @@ class FormFieldValidationTest {
logger = Logger.STANDARD

module(Oidc) {
clientId = "021b83ce-a9b1-4ad4-8c1d-79e576eeab76"
discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration"
clientId = "a6859a12-5e6e-4f64-96bb-cc8577706bee"
discoveryEndpoint = "https://auth.pingone.ca/300c4f2a-39d4-4ba9-a18a-f6de246006f4/as/.well-known/openid-configuration"
scopes = mutableSetOf("openid", "email", "address", "phone", "profile")
redirectUri = "org.forgerock.demo://oauth2redirect"
acrValues = "210f6b876da11c836ffc1c5fb38f3938"
acrValues = "b63ac7fb5db6d893efdd5e29d06a7477"
//storage = dataStore
}
}
Expand Down Expand Up @@ -197,12 +201,130 @@ class FormFieldValidationTest {
assertTrue(passwordValidationResult.isEmpty())
}

@Test
fun passwordMaxRepeatValidationTest() = runTest {
// Go to the "Form Fields Validation" form
var node = daVinci.start() as ContinueNode
(node.collectors[1] as? FlowCollector)?.value = "click"
node = node.next() as ContinueNode

val password = node.collectors[3] as PasswordCollector
val policy = password.passwordPolicy()!!

// Verify the policy actually declares a maxRepeatedCharacters limit
val maxRepeated = policy.maxRepeatedCharacters
assertTrue(maxRepeated < Int.MAX_VALUE, "Test requires a policy with a maxRepeatedCharacters limit")

// Build a value where one character repeats maxRepeated+1 times alongside enough
// other characters to satisfy all other constraints (length, unique, minCharacters)
val excess = "a".repeat(maxRepeated + 1) // e.g. "aaa" for limit=2
val padding = "B1!" // uppercase + digit + symbol
password.value = excess + padding

val errors = password.validate()
assertTrue(errors.contains(MaxRepeat(maxRepeated)),
"Expected MaxRepeat($maxRepeated) in $errors")

// A password with at most maxRepeated repetitions of any character should not trigger MaxRepeat
password.value = "a".repeat(maxRepeated) + "B1!cde"
val errorsAfterFix = password.validate()
assertFalse(errorsAfterFix.contains(MaxRepeat(maxRepeated)),
"MaxRepeat error should be absent when repetitions <= $maxRepeated")
}

@Test
fun passwordMaxLengthValidationTest() = runTest {
// Go to the "Form Fields Validation" form
var node = daVinci.start() as ContinueNode
(node.collectors[1] as? FlowCollector)?.value = "click"
node = node.next() as ContinueNode

val password = node.collectors[3] as PasswordCollector
val policy = password.passwordPolicy()!!
val maxLen = policy.length.max

// Build a value that exceeds the maximum length while satisfying all other constraints
// so that InvalidLength is the only error triggered
val overLength = "Aa1!" + "x".repeat(maxLen) // maxLen+4 chars total
password.value = overLength

val errors = password.validate()
assertTrue(errors.contains(InvalidLength(min = policy.length.min, max = maxLen)),
"Expected InvalidLength for value longer than max=$maxLen")
}

@Test
fun passwordClearPasswordAfterCloseTest() = runTest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect assertNotNull signature — assertion message and value are swapped

assertNotNull("PASSWORD_VERIFY field not found in form components", passwordField.toString())
assertNotNull("passwordPolicy must be embedded inside the PASSWORD_VERIFY field", passwordField!!["passwordPolicy"].toString())

kotlin.test.assertNotNull(message, actual) was deprecated — the two-arg overload is assertNotNull(actual, message). As written, the string literal is always non-null, so this assertion never fails regardless of whether passwordField is actually null.

Also, calling .toString() on a nullable before the null-check defeats the purpose entirely — null.toString() returns "null" (a non-null string), so the assertion passes even when the value is missing.

Fix:

assertNotNull(passwordField, "PASSWORD_VERIFY field not found in form components")
assertNotNull(passwordField!!["passwordPolicy"], "passwordPolicy must be embedded inside the PASSWORD_VERIFY field")

// Go to the "Form Fields Validation" form
var node = daVinci.start() as ContinueNode
(node.collectors[1] as? FlowCollector)?.value = "click"
node = node.next() as ContinueNode

val password = node.collectors[3] as PasswordCollector
password.value = "Password123!"

// Default is clearPassword=true — close() must wipe the value
assertTrue(password.clearPassword)
password.close()
assertEquals("", password.value, "Password should be cleared after close() when clearPassword=true")
}

@Test
fun passwordPolicyAdditionalMetadataTest() = runTest {
// Go to the "Form Fields Validation" form
var node = daVinci.start() as ContinueNode
(node.collectors[1] as? FlowCollector)?.value = "click"
node = node.next() as ContinueNode

val password = node.collectors[3] as PasswordCollector
val policy = password.passwordPolicy()!!

// Verify the fields that drive validation logic but were previously unasserted
assertTrue(policy.maxRepeatedCharacters < Int.MAX_VALUE,
"maxRepeatedCharacters should be set by the 'Standard' policy")
assertEquals(4, policy.minCharacters.size,
"Standard policy should require exactly 4 character classes")

// Verify the read-only metadata fields are populated (not blank/zero defaults)
assertTrue(policy.createdAt.isNotEmpty(), "createdAt should be set")
assertTrue(policy.updatedAt.isNotEmpty(), "updatedAt should be set")
assertTrue(policy.populationCount >= 0, "populationCount should be >= 0")
}

@Test
fun passwordPolicySourceIsFieldLevelTest() = runTest {
// Go to the "Form Fields Validation" form
var node = daVinci.start() as ContinueNode
(node.collectors[1] as? FlowCollector)?.value = "click"
node = node.next() as ContinueNode

val password = node.collectors[3] as PasswordCollector

// Verify the policy is embedded directly inside the PASSWORD_VERIFY field JSON (SDKS-4694).
val passwordField = node.input["form"]
?.jsonObject?.get("components")
?.jsonObject?.get("fields")
?.jsonArray?.firstOrNull {
it.jsonObject["type"]?.jsonPrimitive?.content == "PASSWORD_VERIFY"
}?.jsonObject
assertNotNull("PASSWORD_VERIFY field not found in form components", passwordField.toString())
assertNotNull(
"passwordPolicy must be embedded inside the PASSWORD_VERIFY field",
passwordField!!["passwordPolicy"].toString())
Comment on lines +310 to +313
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="davinci/src/androidTest/kotlin/com/pingidentity/davinci/FormFieldValidationTest.kt"

echo "Checking for message-first assertNotNull usage:"
rg -n 'assertNotNull\(\s*"[^"]+"\s*,\s*' "$FILE"

echo
echo "Checking for toString()-based null assertions:"
rg -n 'assertNotNull\([^)]*toString\(\)' "$FILE"

Repository: ForgeRock/ping-android-sdk

Length of output: 378


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="davinci/src/androidTest/kotlin/com/pingidentity/davinci/FormFieldValidationTest.kt"
nl -ba "$FILE" | sed -n '290,340p'

Repository: ForgeRock/ping-android-sdk

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="davinci/src/androidTest/kotlin/com/pingidentity/davinci/FormFieldValidationTest.kt"

awk 'NR>=290 && NR<=340 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: ForgeRock/ping-android-sdk

Length of output: 2428


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="davinci/src/androidTest/kotlin/com/pingidentity/davinci/FormFieldValidationTest.kt"
awk 'NR<=80{printf "%d:%s\n", NR, $0}' "$FILE"

Repository: ForgeRock/ping-android-sdk

Length of output: 3262


Fix assertNotNull argument order and remove null-masking .toString()/!!

In FormFieldValidationTest.kt lines 310-313, kotlin.test.assertNotNull is called as assertNotNull("message", <value>.toString()), so the non-null message string is used as the actual value (the assertion can pass even when passwordField/passwordPolicy are null). Also, passwordField.toString() / passwordField!!["passwordPolicy"].toString() can throw NPE before the assertion message is surfaced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@davinci/src/androidTest/kotlin/com/pingidentity/davinci/FormFieldValidationTest.kt`
around lines 310 - 313, The two assertions use the wrong argument order and call
.toString()/!! which can throw NPEs before the assertion; change them to call
assertNotNull with the actual nullable values first and the message second, e.g.
assertNotNull(passwordField, "PASSWORD_VERIFY field not found in form
components") and assertNotNull(passwordField?.get("passwordPolicy"),
"passwordPolicy must be embedded inside the PASSWORD_VERIFY field") (remove
.toString() and !! so you assert the raw nullable values instead of masking
nulls).


// The SDK must surface the field-level policy via passwordPolicy()
val policy = password.passwordPolicy()
assertNotNull(policy)
assertEquals("Standard", policy!!.name)
assertEquals(Length(min = 8, max = 255), policy.length)
}

@TestRailCase(27507)
@Test
fun errorNodeTest() = runTest {
// Go to the "Error Node" form
val node = daVinci.start() as ContinueNode
(node.collectors[2] as? FlowCollector)?.value = "click"
(node.collectors[3] as? FlowCollector)?.value = "click"
val errorNode = node.next()
assertTrue(errorNode is ErrorNode)

Expand Down
Loading
Loading