From 3617df3fa32d0163d89d0daf1ff407d7e7390766 Mon Sep 17 00:00:00 2001
From: Stoyan Petrov
Date: Mon, 16 Dec 2024 10:14:34 -0400
Subject: [PATCH 1/3] Prepare device farm artifacts
---
.../workflows/bitbar-prepare-artifacts.yaml | 111 +++++++++++++++++
.github/workflows/bitbar-results.yaml | 104 ++++++++++++++++
.github/workflows/bitbar-run.yaml | 113 ++++++++++++++++++
.github/workflows/ci.yaml | 38 ++++++
.../davinci/DavinciAndroidTest.kt | 2 +-
5 files changed, 367 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/bitbar-prepare-artifacts.yaml
create mode 100644 .github/workflows/bitbar-results.yaml
create mode 100644 .github/workflows/bitbar-run.yaml
diff --git a/.github/workflows/bitbar-prepare-artifacts.yaml b/.github/workflows/bitbar-prepare-artifacts.yaml
new file mode 100644
index 0000000..5f41c7d
--- /dev/null
+++ b/.github/workflows/bitbar-prepare-artifacts.yaml
@@ -0,0 +1,111 @@
+#
+# Copyright (c) 2024 Ping Identity. All rights reserved.
+#
+# This software may be modified and distributed under the terms
+# of the MIT license. See the LICENSE file for details.
+#
+
+name: Prepare BitBar Artifacts
+on:
+ workflow_call:
+ secrets:
+ SIGNING_KEYSTORE:
+ description: 'Needed for signing the apk artifacts'
+ required: true
+ SIGNING_ALIAS:
+ description: 'Needed for signing the apk artifacts'
+ required: true
+ SIGNING_KEYSTORE_PASSWORD:
+ description: 'Needed for signing the apk artifacts'
+ required: true
+ SIGNING_KEY_PASSWORD:
+ description: 'Needed for signing the apk artifacts'
+ required: true
+ SLACK_WEBHOOK:
+ description: Slack Notifier Incoming Webhook
+ required: true
+jobs:
+ prepare-device-farm-artifacts:
+ runs-on: macos-latest
+
+ steps:
+ # Clone the repo
+ - name: Clone the repository
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+ fetch-depth: 0
+
+ # Setup JDK and cache and restore dependencies.
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ cache: 'gradle'
+
+ # Build apk files
+ - name: Prepare device farm artifacts
+ run: ./gradlew assembleDebugAndroidTest --stacktrace --no-daemon
+
+ # List the available build tools versions see https://github.com/r0adkll/sign-android-release/issues/84
+ - name: List build tools versions
+ run: ls /Users/runner/Library/Android/sdk/build-tools/
+
+ # Sign app-debug-androidTest.apk
+ - name: Sign app-debug-androidTest.apk
+ uses: r0adkll/sign-android-release@v1
+ with:
+ releaseDirectory: samples/app/build/outputs/apk/androidTest/debug
+ signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }}
+ alias: ${{ secrets.SIGNING_ALIAS }}
+ keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
+ keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
+ env:
+ BUILD_TOOLS_VERSION: "35.0.0"
+
+ # Sign davinci-debug-androidTest.apk
+ - name: Sign davinci-debug-androidTest.apk
+ uses: r0adkll/sign-android-release@v1
+ with:
+ releaseDirectory: davinci/build/outputs/apk/androidTest/debug
+ signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }}
+ alias: ${{ secrets.SIGNING_ALIAS }}
+ keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
+ keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
+ env:
+ BUILD_TOOLS_VERSION: "35.0.0"
+
+ # Publish the signed APKs as build artifacts
+ - name: Publish app-debug-androidTest.apk
+ uses: actions/upload-artifact@v4
+ if: success()
+ with:
+ name: app-debug-androidTest-signed.apk
+ path: samples/app/build/outputs/apk/androidTest/debug/app-debug-androidTest-signed.apk
+
+ - name: Publish davinci-debug-androidTest.apk
+ uses: actions/upload-artifact@v4
+ if: success()
+ with:
+ name: davinci-debug-androidTest-signed.apk
+ path: davinci/build/outputs/apk/androidTest/debug/davinci-debug-androidTest-signed.apk
+
+ # Send slack notification ONLY if any of the steps above fail
+ - name: Send slack notification
+ uses: 8398a7/action-slack@v3
+ with:
+ status: custom
+ fields: all
+ custom_payload: |
+ {
+ attachments: [{
+ title: ':no_entry: Failed to prepare BitBar test artifacts',
+ color: 'danger',
+ text: `\nWorkflow: ${process.env.AS_WORKFLOW} -> ${process.env.AS_JOB}\nPull request: ${process.env.AS_PULL_REQUEST}\nCommit: ${process.env.AS_COMMIT} by ${process.env.AS_AUTHOR}`,
+ }]
+ }
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+ if: failure()
\ No newline at end of file
diff --git a/.github/workflows/bitbar-results.yaml b/.github/workflows/bitbar-results.yaml
new file mode 100644
index 0000000..e743df1
--- /dev/null
+++ b/.github/workflows/bitbar-results.yaml
@@ -0,0 +1,104 @@
+#
+# Copyright (c) 2024 Ping Identity. All rights reserved.
+#
+# This software may be modified and distributed under the terms
+# of the MIT license. See the LICENSE file for details.
+#
+
+name: BitBar Test Results
+
+on:
+ workflow_call:
+ inputs:
+ bitbar-project-id:
+ description: BitBar Project ID
+ type: string
+ required: true
+ bitbar-run-id:
+ description: BitBar Run ID
+ type: string
+ required: true
+ outputs:
+ bitbar-run-url:
+ description: "The BitBar run URL"
+ value: ${{ jobs.bitbar-run.outputs.bitbar_run_url }}
+ secrets:
+ BITBAR_API_KEY:
+ description: BitBar API Key
+ required: true
+ SLACK_WEBHOOK:
+ description: Slack Notifier Incoming Webhook
+ required: true
+
+jobs:
+ bitbar-results:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Workflow inputs:"
+ run: |
+ echo "Project ID - ${{ inputs.bitbar-project-id }}"
+ echo "Run ID - ${{ inputs.bitbar-run-id }}"
+
+ - name: Wait for BitBar test run to finish...
+ timeout-minutes: 60
+ run: |
+ (
+ until [ "$(curl -s -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/projects/${{ inputs.bitbar-project-id }}/runs/${{ inputs.bitbar-run-id }} | jq -r '.state')" == "FINISHED" ];
+ do
+ echo "Waiting for BitBar Results. Sleeping for 10 seconds..."
+ sleep 10
+ done
+ )
+ echo "BITBAR_TEST_RUN_RESULT=$(curl -s -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/projects/${{ inputs.bitbar-project-id }}/runs/${{ inputs.bitbar-run-id }})" >> $GITHUB_ENV
+
+ # Get the outcome json of the test run.
+ - name: Parse test run outcome json
+ run: |
+ echo ${{ env.BITBAR_TEST_RUN_RESULT }}
+ echo "==========================================="
+ echo "projectName: $(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq '.projectName')"
+ echo "displayName: $(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq '.displayName')"
+ echo "executedTestCaseCount: $(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq '.executedTestCaseCount')"
+ echo "successfulTestCaseCount: $(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq '.successfulTestCaseCount')"
+ echo "failedTestCaseCount: $(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq '.failedTestCaseCount')"
+ echo "runningDeviceCount: $(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq '.runningDeviceCount')"
+ echo "totalDeviceCount: $(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq '.totalDeviceCount')"
+ echo "==========================================="
+ echo "BITBAR_PROJECT_NAME=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.projectName')" >> $GITHUB_ENV
+ echo "BITBAR_RUN_DISPLAY_NAME=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.displayName')" >> $GITHUB_ENV
+ echo "BITBAR_RUN_NUMBER=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.number')" >> $GITHUB_ENV
+ echo "BITBAR_EXECUTED_TESTS_COUNT=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.executedTestCaseCount')" >> $GITHUB_ENV
+ echo "BITBAR_SUCCESS_TESTS_COUNT=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.successfulTestCaseCount')" >> $GITHUB_ENV
+ echo "BITBAR_FAILED_TESTS_COUNT=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.failedTestCaseCount')" >> $GITHUB_ENV
+ echo "BITBAR_SUCCESS_RATIO=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.successRatio')" >> $GITHUB_ENV
+ echo "BITBAR_DEVICE_COUNT=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.deviceCount')" >> $GITHUB_ENV
+ echo "BITBAR_DEVICE_GROUP_NAME=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.config.usedDeviceGroupName')" >> $GITHUB_ENV
+ echo "BITBAR_TEST_RUN_URL=$(echo '${{ env.BITBAR_TEST_RUN_RESULT }}' | jq -r '.uiLink')" >> $GITHUB_ENV
+
+ # Check for failures and set the outcome of the workflow
+ - name: Set job status
+ run: |
+ if [[ ${{env.BITBAR_FAILED_TESTS_COUNT}} != '0' ]]; then
+ exit 1
+ else
+ exit 0
+ fi
+
+ # Send slack notification with result status
+ - name: Send slack notification
+ uses: 8398a7/action-slack@v3
+ with:
+ status: custom
+ fields: all
+ custom_payload: |
+ {
+ attachments: [{
+ title: 'BitBar ${{ env.BITBAR_PROJECT_NAME }} - #${{ env.BITBAR_RUN_NUMBER }}',
+ title_link: '${{ env.BITBAR_TEST_RUN_URL }}',
+ color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
+ text: `\nTest summary: ${{ job.status }} in ${process.env.AS_TOOK}\nPassed: ${{ env.BITBAR_SUCCESS_TESTS_COUNT }}, Failed: ${{ env.BITBAR_FAILED_TESTS_COUNT }}\nDevice group: ${{ env.BITBAR_DEVICE_GROUP_NAME }}, Number of devices: ${{ env.BITBAR_DEVICE_COUNT }}\n\nWorkflow: ${process.env.AS_WORKFLOW} -> ${process.env.AS_JOB}\nPull request: ${process.env.AS_PULL_REQUEST}\nCommit: ${process.env.AS_COMMIT} by ${process.env.AS_AUTHOR}\nMessage: ${process.env.AS_MESSAGE}`,
+ }]
+ }
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+ if: always()
\ No newline at end of file
diff --git a/.github/workflows/bitbar-run.yaml b/.github/workflows/bitbar-run.yaml
new file mode 100644
index 0000000..596ece8
--- /dev/null
+++ b/.github/workflows/bitbar-run.yaml
@@ -0,0 +1,113 @@
+#
+# Copyright (c) 2024 Ping Identity. All rights reserved.
+#
+# This software may be modified and distributed under the terms
+# of the MIT license. See the LICENSE file for details.
+#
+
+name: Run Tests in BitBar
+on:
+ workflow_call:
+ inputs:
+ bitbar-project-id:
+ description: BitBar project id
+ type: string
+ bitbar-device-group-id:
+ description: The device group id to run tests against
+ type: string
+ bitbar-os-type:
+ description: OS Type
+ type: string
+ default: ANDROID
+ bitbar-framework-id:
+ description: The framework id
+ type: string
+ default: 252
+ outputs:
+ bitbar-run-id:
+ description: The newly created run id in BitBar
+ value: ${{ jobs.bitbar-run.outputs.bitbar_run_id }}
+ secrets:
+ BITBAR_API_KEY:
+ description: BitBar API Key
+ required: true
+ SLACK_WEBHOOK:
+ description: Slack Notifier Incoming Webhook
+ required: true
+jobs:
+ bitbar-run:
+ runs-on: ubuntu-latest
+ outputs:
+ bitbar_run_id: ${{ steps.bitbar_run_id.outputs.bitbar_run_id }}
+
+ steps:
+ # Get the test artifacts prepared in previous step
+ - name: Get the app-debug-androidTest-signed.apk BitBar artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: app-debug-androidTest-signed.apk
+
+ - name: Get the davinci-debug-androidTest-signed.apk BitBar artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: davinci-debug-androidTest-signed.apk
+
+ - name: Unzip app-debug-androidTest-signed.apk and davinci-debug-androidTest-signed.apk
+ run: |
+ unzip -o app-debug-androidTest-signed.apk
+ unzip -o davinci-debug-androidTest-signed.apk
+
+ - name: Upload app-debug-androidTest-signed.apk to BitBar
+ run: |
+ echo "BITBAR_APP_FILE_ID=$(curl -X POST -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/files -F "file=@app-debug-androidTest-signed.apk" | jq '.id')" >> $GITHUB_ENV
+
+ - name: Upload davinci-debug-androidTest-signed.apk to BitBar
+ run: |
+ echo "BITBAR_TEST_FILE_ID=$(curl -X POST -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/files -F "file=@davinci-debug-androidTest-signed.apk" | jq '.id')" >> $GITHUB_ENV
+
+ - name: Prepare BitBar run configuration file
+ run: |
+ (
+ echo "{"
+ echo "\"osType\":\"${{ inputs.bitbar-os-type }}\","
+ echo "\"projectId\":${{ inputs.bitbar-project-id }},"
+ echo "\"frameworkId\":${{ inputs.bitbar-framework-id }},"
+ echo "\"deviceGroupId\":${{ inputs.bitbar-device-group-id }},"
+ echo "\"files\":["
+ echo " {\"id\":${{ env.BITBAR_APP_FILE_ID }}, \"action\": \"INSTALL\"},"
+ echo " {\"id\":${{ env.BITBAR_TEST_FILE_ID }}, \"action\": \"RUN_TEST\"}"
+ echo "]"
+ echo "}"
+ ) > bitbar-run-configuration.txt
+
+ - name: Display bitbar-run-configuration.txt
+ run: |
+ cat bitbar-run-configuration.txt
+
+ # Start the test run
+ - name: Start a test run
+ run: |
+ echo "BITBAR_TEST_RUN_ID=$(curl -H 'Content-Type: application/json' -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/runs --data-binary @bitbar-run-configuration.txt | jq '.id')" >> $GITHUB_ENV
+
+ # Set bitbar_run_id as output of the workflow. This is needed for the next workflow to continue
+ - name: Set the bitbar_run_id output
+ id: bitbar_run_id
+ run: echo "::set-output name=bitbar_run_id::${{ env.BITBAR_TEST_RUN_ID }}"
+
+ # Send slack notification ONLY if any of the steps above fail
+ - name: Send slack notification
+ uses: 8398a7/action-slack@v3
+ with:
+ status: custom
+ fields: all
+ custom_payload: |
+ {
+ attachments: [{
+ title: ':no_entry: Failed to start BitBar test run!',
+ color: 'danger',
+ text: `\nWorkflow: ${process.env.AS_WORKFLOW} -> ${process.env.AS_JOB}\nPull request: ${process.env.AS_PULL_REQUEST}\nCommit: ${process.env.AS_COMMIT} by ${process.env.AS_AUTHOR}`,
+ }]
+ }
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+ if: failure()
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b1080e8..47b57f7 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -56,6 +56,44 @@ jobs:
runs-on: ubuntu-latest
testrail-run-id: ${{needs.create-testrail-run.outputs.testrail-run-id}}
+ # Build and sign BitBar test artifacts (app-debug-androidTest-signed.apk and davinci-debug-androidTest-signed.apk)
+ # Skip this step for PRs created by dependabot
+ bitbar-prepare-artifacts:
+ name: Prepare device farm artifacts
+ uses: ./.github/workflows/bitbar-prepare-artifacts.yaml
+ if: ${{ github.actor != 'dependabot[bot]' }}
+ needs: build-and-test
+ secrets:
+ SIGNING_KEYSTORE: ${{ secrets.SIGNING_KEYSTORE }}
+ SIGNING_ALIAS: ${{ secrets.SIGNING_ALIAS }}
+ SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
+ SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
+ SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+
+ # Execute e2e test cases in BitBar. The workflow outputs the newly created run id.
+ bitbar-run:
+ name: Run tests in BitBar
+ uses: ./.github/workflows/bitbar-run.yaml
+ needs: bitbar-prepare-artifacts
+ secrets:
+ BITBAR_API_KEY: ${{ secrets.BITBAR_API_KEY }}
+ SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+ with:
+ bitbar-project-id: ${{ vars.BITBAR_PROJECT_ID }}
+ bitbar-device-group-id: ${{ vars.BITBAR_DEVICE_GROUP_ID }}
+
+ # Wait for BitBar test run to finish and publish results
+ bitbar-results:
+ name: BitBar test results
+ uses: ./.github/workflows/bitbar-results.yaml
+ needs: bitbar-run
+ secrets:
+ BITBAR_API_KEY: ${{ secrets.BITBAR_API_KEY }}
+ SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+ with:
+ bitbar-project-id: ${{ vars.BITBAR_PROJECT_ID }}
+ bitbar-run-id: ${{ needs.bitbar-run.outputs.bitbar-run-id }}
+
# Run Mend CLI Scan
mend-cli-scan:
name: Mend CLI Scan
diff --git a/davinci/src/androidTest/kotlin/com/pingidentity/davinci/DavinciAndroidTest.kt b/davinci/src/androidTest/kotlin/com/pingidentity/davinci/DavinciAndroidTest.kt
index 9bc8646..02f0b73 100644
--- a/davinci/src/androidTest/kotlin/com/pingidentity/davinci/DavinciAndroidTest.kt
+++ b/davinci/src/androidTest/kotlin/com/pingidentity/davinci/DavinciAndroidTest.kt
@@ -607,7 +607,7 @@ class DavinciAndroidTest {
}
@TestRailCase(24629)
- @Test(timeout = 20000)
+ @Test(timeout = 60000)
fun accountLocked() = runBlocking {
// Register a test user...
val newUser = userFname + System.currentTimeMillis() + "@example.com"
From ffcea6320be8d65c1ba035edf8dcc7432c4d7a34 Mon Sep 17 00:00:00 2001
From: Andy Witrisna
Date: Thu, 19 Dec 2024 12:11:43 -0800
Subject: [PATCH 2/3] SDKS-3616: Prevent side effects on the Global Logger when
configuring the DaVinci Logger.
---
.../kotlin/com/pingidentity/orchestrate/WorkflowConfig.kt | 4 ----
1 file changed, 4 deletions(-)
diff --git a/foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/WorkflowConfig.kt b/foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/WorkflowConfig.kt
index afe5990..414dc0c 100644
--- a/foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/WorkflowConfig.kt
+++ b/foundation/orchestrate/src/main/kotlin/com/pingidentity/orchestrate/WorkflowConfig.kt
@@ -41,10 +41,6 @@ open class WorkflowConfig {
// Logger for the log, default is None
var logger = Logger.logger
- set(value) {
- field = value
- Logger.logger = value
- }
// HTTP client for the engine
lateinit var httpClient: HttpClient
From 7854c67abcac93b384d9f986e6ec8619662445d8 Mon Sep 17 00:00:00 2001
From: Andy Witrisna
Date: Tue, 7 Jan 2025 18:57:01 -0800
Subject: [PATCH 3/3] SDKS-3596 DaVinci Form Implementation
- Require and Regex Validation
- Provide function to access ErrorNode with validation error (e.g Password policy not met)
- Handle Multi and single select collector, checkbox, dropdown, radio, combobox
- New Collectors, LABEL, SingleSelect, MultiSelect
- Handle default formData, populate default value from server.
---
davinci/CONCEPT.md | 134 +++++++++++
davinci/README.md | 196 ++++++++++++---
davinci/build.gradle.kts | 4 +-
davinci/images/davinciSequence.png | Bin 105217 -> 0 bytes
.../pingidentity/davinci/CollectorRegistry.kt | 17 +-
.../kotlin/com/pingidentity/davinci/Json.kt | 16 ++
.../davinci/collector/Collectors.kt | 84 ++++---
.../davinci/collector/FieldCollector.kt | 21 +-
.../davinci/collector/FlowCollector.kt | 6 +-
.../pingidentity/davinci/collector/Form.kt | 14 +-
.../davinci/collector/LabelCollector.kt | 30 +++
.../davinci/collector/MultiSelectCollector.kt | 58 +++++
.../pingidentity/davinci/collector/Option.kt | 34 +++
.../davinci/collector/PasswordCollector.kt | 60 ++++-
.../davinci/collector/PasswordPolicy.kt | 81 +++++++
.../collector/SingleSelectCollector.kt | 28 +++
.../davinci/collector/SingleValueCollector.kt | 34 +++
.../davinci/collector/SubmitCollector.kt | 6 +-
.../davinci/collector/TextCollector.kt | 6 +-
.../davinci/collector/ValidatedCollector.kt | 55 +++++
.../davinci/collector/ValidationError.kt | 26 ++
.../pingidentity/davinci/module/Connector.kt | 4 +-
.../pingidentity/davinci/module/ErrorNode.kt | 112 +++++++++
.../davinci/CollectorRegistryTest.kt | 23 +-
.../pingidentity/davinci/DaVinciErrorTest.kt | 78 +++++-
.../com/pingidentity/davinci/DaVinciTest.kt | 131 +++++++++-
.../davinci/FieldCollectorTest.kt | 9 +-
.../pingidentity/davinci/FlowCollectorTest.kt | 33 ++-
.../davinci/LabelCollectorTest.kt | 37 +++
.../davinci/MultiSelectCollectorTest.kt | 78 ++++++
.../davinci/PasswordCollectorTest.kt | 130 +++++++++-
.../davinci/PasswordPolicyTest.kt | 101 ++++++++
.../davinci/SingleSelectCollectorTest.kt | 56 +++++
.../pingidentity/davinci/TextCollectorTest.kt | 18 +-
.../davinci/ValidatedCollectorTest.kt | 79 ++++++
.../resources/PasswordValidationError.json | 48 ++++
.../test/resources/ResponseWithBasicType.json | 225 ++++++++++++++++++
.../pingidentity/davinci/plugin/Collector.kt | 11 +-
.../kotlin/com/pingidentity/test/Utils.kt | 17 ++
.../com/pingidentity/samples/app/Alert.kt | 43 +++-
.../com/pingidentity/samples/app/AppDrawer.kt | 2 +-
.../pingidentity/samples/app/AppNavHost.kt | 2 +-
.../samples/app/LogoutViewModel.kt | 2 +-
.../com/pingidentity/samples/app/Setting.kt | 2 +-
.../java/com/pingidentity/samples/app/User.kt | 2 +-
.../centralize/CentralizeLoginViewModel.kt | 2 +-
.../samples/app/davinci/DaVinci.kt | 21 +-
.../samples/app/davinci/DaVinciViewModel.kt | 2 +-
.../samples/app/davinci/collector/CheckBox.kt | 69 ++++++
.../samples/app/davinci/collector/ComboBox.kt | 105 ++++++++
.../app/davinci/collector/ContinueNode.kt | 22 +-
.../samples/app/davinci/collector/Dropdown.kt | 75 ++++++
.../app/davinci/collector/ErrorMessage.kt | 49 ++++
.../app/davinci/collector/FlowButton.kt | 41 +++-
.../samples/app/davinci/collector/Label.kt | 39 +++
.../samples/app/davinci/collector/Password.kt | 96 +++++++-
.../samples/app/davinci/collector/Radio.kt | 59 +++++
.../samples/app/davinci/collector/Text.kt | 26 +-
.../com/pingidentity/samples/app/env/Env.kt | 2 +-
.../samples/app/env/EnvViewModel.kt | 14 +-
60 files changed, 2636 insertions(+), 139 deletions(-)
create mode 100644 davinci/CONCEPT.md
delete mode 100644 davinci/images/davinciSequence.png
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/Json.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/LabelCollector.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/MultiSelectCollector.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/Option.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/PasswordPolicy.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/SingleSelectCollector.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/SingleValueCollector.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/ValidatedCollector.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/collector/ValidationError.kt
create mode 100644 davinci/src/main/kotlin/com/pingidentity/davinci/module/ErrorNode.kt
create mode 100644 davinci/src/test/kotlin/com/pingidentity/davinci/LabelCollectorTest.kt
create mode 100644 davinci/src/test/kotlin/com/pingidentity/davinci/MultiSelectCollectorTest.kt
create mode 100644 davinci/src/test/kotlin/com/pingidentity/davinci/PasswordPolicyTest.kt
create mode 100644 davinci/src/test/kotlin/com/pingidentity/davinci/SingleSelectCollectorTest.kt
create mode 100644 davinci/src/test/kotlin/com/pingidentity/davinci/ValidatedCollectorTest.kt
create mode 100644 davinci/src/test/resources/PasswordValidationError.json
create mode 100644 davinci/src/test/resources/ResponseWithBasicType.json
create mode 100644 foundation/testrail/src/main/kotlin/com/pingidentity/test/Utils.kt
create mode 100644 samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/CheckBox.kt
create mode 100644 samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/ComboBox.kt
create mode 100644 samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Dropdown.kt
create mode 100644 samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/ErrorMessage.kt
create mode 100644 samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Label.kt
create mode 100644 samples/app/src/main/java/com/pingidentity/samples/app/davinci/collector/Radio.kt
diff --git a/davinci/CONCEPT.md b/davinci/CONCEPT.md
new file mode 100644
index 0000000..791d307
--- /dev/null
+++ b/davinci/CONCEPT.md
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+# Design Concept
+
+## Mapping Field Types to Collectors
+
+DaVinci employs a factory-based approach to dynamically create and initialize collectors for different field types in a
+form. This allows us to handle various field types such as text fields, password fields, and buttons, each with its own
+specific behavior, validation, and data collection logic.
+
+The `CollectorFactory` is a factory class that maps field types to their corresponding collectors. To register a new
+collector, you provide the field type and the constructor reference of the collector. Using the constructor reference,
+the `CollectorFactory` can dynamically create collectors when parsing the DaVinci Response JSON.
+
+```
+CollectorFactory.register(, )
+```
+
+For example:
+
+```kotlin
+// Map Password Type to PasswordCollector
+CollectorFactory.register("PASSWORD", ::PasswordCollector)
+
+CollectorFactory.register("SUBMIT_BUTTON", ::SubmitCollector)
+
+// Allow to map multiple Field Type to the same Collector
+CollectorFactory.register("FLOW_BUTTON", ::FlowCollector)
+CollectorFactory.register("FLOW_LINK", ::FlowCollector)
+```
+
+## How Collectors Are Created and Initialized
+
+DaVinci Response JSON:
+
+```json
+{
+ "form": {
+ "components": {
+ "fields": [
+ {
+ "type": "TEXT",
+ "key": "user.username",
+ "label": "Username",
+ "required": true,
+ "validation": {
+ "regex": "^[^@]+@[^@]+\\.[^@]+$",
+ "errorMessage": "Must be alphanumeric"
+ }
+ },
+ {
+ "type": "PASSWORD",
+ "key": "password",
+ "label": "Password",
+ "required": true
+ },
+ ...
+ ]
+ }
+ }
+}
+```
+
+```mermaid
+sequenceDiagram
+ Form ->> CollectorFactory: collector(fields)
+ loop ForEach Field in Fields
+ CollectorFactory ->> Collector: create()
+ CollectorFactory ->> Collector: init(field)
+ Collector ->> Collector: populate the instance properties with field Json
+ end
+ CollectorFactory ->> Form: collectors
+```
+
+## How Collectors Populate Default Values
+
+The Collector populates the default value from the `formData` JSON:
+
+```json
+{
+ "formData": {
+ "value": {
+ "user.username": "",
+ "password": "",
+ "dropdown-field": "",
+ "combobox-field": [],
+ "radio-field": "",
+ "checkbox-field": []
+ }
+ }
+}
+```
+
+```mermaid
+sequenceDiagram
+ loop ForEach Collector in Collectors
+ Form ->> Collector: getKey()
+ alt key in formData
+ Form ->> Collector: init(formData[key])
+ Collector ->> Collector: "populate the default value with formData
+ end
+ end
+```
+
+## How Collectors Access the ConnectorNode
+
+By default, the `Collector` is self-contained and does not have direct access to the `ConnectorNode`. It is responsible for
+handling data collection and validation specific to the field it represents. However, in certain cases, a collector may
+need to access the `ConnectorNode` - for example, to retrieve global data such as the `passwordPolicy` from the root JSON to
+validate a password.
+
+To enable this access, a collector can implement the `ConnectorNodeAware` interface. This interface includes the
+`connectorNode` property, allowing the collector to retrieve the `ConnectorNode` when needed. Once a collector is created,
+the `ConnectorNode` is injected into it, granting access to global data for tasks like validation or other cross-field
+operations.
+
+```kotlin
+class PasswordCollector : ContinueNodeAware {
+ override lateinit var continueNode: ContinueNode
+}
+```
+```mermaid
+sequenceDiagram
+ loop ForEach Collector in ContinueNode.collectors
+ alt Collector is ContinueNodeAware
+ CollectorFactory ->> Collector: setContinueNode(continueNode)
+ end
+ end
+```
\ No newline at end of file
diff --git a/davinci/README.md b/davinci/README.md
index 48cbe3d..c35ce62 100644
--- a/davinci/README.md
+++ b/davinci/README.md
@@ -13,7 +13,27 @@ DaVinci is a powerful and flexible library for Authentication and Authorization.
extensible. It provides a simple API for navigating the authentication flow and handling the various states that can
occur during the authentication process.
-
+```mermaid
+sequenceDiagram
+ Developer ->> DaVinci: Create DaVinci instance
+ DaVinci ->> Developer: DaVinci
+ Developer ->> DaVinci: start()
+ DaVinci ->> PingOne DaVinci Server: /authorize
+ PingOne DaVinci Server ->> PingOne DaVinci Server: Launch DaVinci Flow
+ PingOne DaVinci Server ->> DaVinci: Node with Form
+ DaVinci ->> Developer: Node
+ Developer ->> Developer: Gather credentials
+ Developer ->> DaVinci: next()
+ DaVinci ->> PingOne DaVinci Server: /continue
+ PingOne DaVinci Server ->> DaVinci: authorization code
+ DaVinci ->> PingOne DaVinci Server: /token
+ PingOne DaVinci Server ->> DaVinci: access token
+ DaVinci ->> DaVinci: persist access token
+ DaVinci ->> Developer: Access Token
+```
+
+You can find more information about PingOne
+DaVinci [here](https://docs.pingidentity.com/davinci/davinci_introduction.html).
## Add dependency to your project
@@ -77,46 +97,150 @@ when (node) {
}
```
-| Node Type | Description |
-|-------------|:-----------------------------------------------------------------------------------------------------------|
-| ContinueNode | In the middle of the flow, call ```node.next``` to move to next Node in the flow |
-| ErrorNode | Bad Request from the server, e.g Invalid Password, OTP, username ```node.message``` for the error message |
-| FailureNode | Unexpected Error, e.g Network, parsing ```node.cause``` to retrieve the cause of the error |
-| SuccessNode | Authentication successful ```node.session``` to retrieve the session |
+| Node Type | Description |
+|--------------|:---------------------------------------------------------------------------------------------------------------------|
+| ContinueNode | In the middle of the flow, call ```node.next``` to move to next Node in the flow |
+| ErrorNode | Bad request from the server, e.g., invalid password, OTP, or username. Use ```node.message``` for the error message. |
+| FailureNode | Unexpected error, e.g., network issues. Use node.cause to retrieve the cause of the error. |
+| SuccessNode | Successful authentication. Use ```node.session``` to retrieve the session. |
### Provide Input
-For `ContinueNode` Node, you can access list of Collector with `node.collectors()` and provide input to
-the `Collector`.
-Currently, there are, `TextCollector`, `PasswordCollector`, `SubmitCollector`, `FlowCollector`, but more will be added in the future, such as `Fido`,
-`SocialLoginCollector`, etc...
+For a `ContinueNode`, you can access the list of collectors using `node.collectors()` and provide input to the desired
+`Collector`.
+Currently, the available collectors include `TextCollector`, `PasswordCollector`, `SubmitCollector`, `FlowCollector`,
+`LabelCollector`, `MultiSelectCollector`, and `SingleSelectCollector`. Additional collectors, such as `Fido` and
+`IdpCollector`, will be added in the future.
To access the collectors, you can use the following code:
```kotlin
node.collectors.forEach {
- when(it) {
- is TextCollector -> it.value = "My First Name"
- is PasswordCollector -> it.value = "My Password"
- is SubmitCollector -> it.value = "click me"
- is FlowCollector -> it.value = "Forgot Password"
- }
+ when (it) {
+ is TextCollector -> it.value = "My First Name"
+ is PasswordCollector -> it.value = "My Password"
+ is SubmitCollector -> it.value = "click me"
+ is FlowCollector -> it.value = "Forgot Password"
+ ...
}
+}
+
+// Move to next Node, and repeat the flow until it reaches `SuccessNode` or `ErrorNode`
+val next = node.next()
+```
+
+Each `Collector` has its own function.
+
+#### TextCollector (TEXT)
+
+```kotlin
+textCollector.label //To access the label
+textCollector.key //To access the key attribute
+textCollector.type //To access the type attribute
+textCollector.required //To access the required attribute
+textCollector.valiation //To access the validation attribute
+
+textCollector.validate() //To validate the field's input value using both required and regex constraints.
+textCollector.value = "My First Name" //To set the value
+```
+
+#### PasswordCollector (PASSWORD, PASSWORD_VERIFY)
+
+`PasswordCollector` has the same attributes as `TextCollector`, plus the following functions
+
+```kotlin
+passwordCollector.passwordPolicy() //Retrieve the password policy
+passwordCollector.validate() //To validate the field input value against the password policy
+
+passwordCollector.type == "PASSWORD_VERIFY" // Check if the type is "PASSWORD_VERIFY".
+```
+
+#### SubmitCollector (SUBMIT_BUTTON)
+
+```kotlin
+submitCollector.label //To access the label
+submitCollector.key //To access the key attribute
+submitCollector.type //To access the type attribute
+submitCollector.value = "submit" //To set the value
+```
+
+#### FlowCollector (FLOW_BUTTON, FLOW_LINK)
+
+`FlowCollector` has the same attributes as `SubmitCollector`
+
+```kotlin
+flowCollector.type == "FLOW_LINK" // Check if the type is "FLOW_LINK". Note that developers may choose to display flow collectors as link or button.
+```
+
+#### LabelCollector (LABEL)
- // Move to next Node, and repeat the flow until it reaches `SuccessNode` or `ErrorNode`
- val next = node.next()
+```kotlin
+labelCollector.content //To access the Content
+```
+
+#### MultiSelectCollector (COMBOBOX, CHECKBOX)
+
+```kotlin
+multiSelectCollector.label //To access the label
+multiSelectCollector.key //To access the key attribute
+multiSelectCollector.type //To access the type attribute
+multiSelectCollector.required //To access the required attribute
+multiSelectCollector.options //To access the options attribute
+
+multiSelectCollector.value.add("option1") //To add the value
+```
+
+#### SingleSelectCollector (DROPDOWN, RADIO)
+
+```kotlin
+singleSelectCollector.label //To access the label
+singleSelectCollector.key //To access the key attribute
+singleSelectCollector.type //To access the type attribute
+singleSelectCollector.required //To access the required attribute
+singleSelectCollector.options //To access the options attribute
+
+singleSelectCollector.value = "option1" //To set the value
```
+### Collector Validation
+
+Collectors have a `validate()` function to validate the input value. The `validate()` function will return `Success`
+or `List`
+
+For example, to validate the `TextCollector` input value, you can use the following code:
+
+```kotlin
+val result: List = textCollector.validate()
+```
+
+| ValidationError | Description |
+|-----------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| InvalidLength | Indicates that the password length is outside the valid range. The InvalidLength error includes the required minimum and maximum lengths. |
+| UniqueCharacter | Indicates that the number of unique characters is less than the required minUniqueCharacters specified in the the policy. The UniqueCharacter error contains the required minimum unique character count. |
+| MaxRepeat | Indicates that the maxRepeatedCharacters policy requirement is not met. The MaxRepeat error specifies the maximum allowed repetitions of a character. |
+| MinCharacters | Indicates that the minCharacters password policy requirement is not met. |
+| Required | Indicates that the input value has not been supplied, but is required. |
+| Regex | Indicates that the input value does not match the required pattern. The Regex error contains the required regular expression. |
+
### Error Handling
-For `FailureNode`, you can retrieve the cause of the error by using `node.cause()`. The `cause` is a `Throwable` object,
-when receiving an error, you cannot continue the Flow, you may want to display a generic message to user, and report
-the issue to the Support team.
-The Error may include Network issue, parsing issue, API Error (Server response other that 2xx and 400) and other unexpected issues.
+`FailureNode` and `ErrorNode` handle errors differently in the flow. A `FailureNode` represents an unrecoverable error
+that
+prevents the flow from continuing, whereas an `ErrorNode` allows the flow to continue and provides an error message for
+the user.
-For `ErrorNode`, you can retrieve the error message by using `node.message()`, and the raw json response with `node.input`.
-The `message` is a `String` object, when receiving a failure, you can continue the Flow with previous `ContinueNode` Node, but you may want to display the error message to the user.
-e.g "Username/Password is incorrect", "OTP is invalid", etc...
+For a `FailureNode`, you can retrieve the cause of the error using `node.cause()`. The cause is a `Throwable` object.
+When an
+error occurs, the flow cannot continue, and you may want to display a generic message to the user and report the issue
+to the support team. Possible errors include network issues, parsing problems, API errors (e.g., server responses in the
+5xx range), and other unexpected issues.
+
+For an `ErrorNode`, you can retrieve the error message using `node.message()` and access the raw JSON response with
+node.input.
+
+The `message` is a `String` object. When a failure occurs, you can continue the flow with the previous `ContinueNode`,
+but you
+may want to display the error message to the user (e.g., "Username/Password is incorrect", "OTP is invalid", etc.).
```kotlin
val node = daVinci.start() // Start the flow
@@ -126,6 +250,16 @@ when (node) {
is ContinueNode -> {}
is ErrorNode -> {
node.message() // Retrieve the cause of the error
+ node.details().forEach { // Retrieve the details of the error
+ it.rawResponse.let { rawResponse ->
+ rawResponse.details?.forEach { detail ->
+ val msg = detail.message
+ detail.innerError?.errors?.forEach { (key, value) ->
+ val innerError = "$key: $value"
+ }
+ }
+ }
+ }
}
is FailureNode -> {
node.cause() // Retrieve the error message
@@ -154,11 +288,13 @@ when (node.id()) {
}
```
-Other than `id`, you can also use `node.name` to retrieve the name of the Node, `node.description` to retrieve the description of the Node.
+Other than `id`, you can also use `node.name` to retrieve the name of the Node, `node.description` to retrieve the
+description of the Node.
### Work with Jetpack Composable
ViewModel
+
```kotlin
// Define State that listen by the View
var state = MutableStateFlow(Empty)
@@ -182,6 +318,7 @@ fun next(node: ContinueNode) {
```
View
+
```kotlin
when (val node = state.node) {
is ContinueNode -> {}
@@ -192,6 +329,7 @@ when (val node = state.node) {
```
### Post Authentication
+
After authenticate with DaVinci, the user session will be stored in the storage.
To retrieve the existing session, you can use the following code:
@@ -207,6 +345,4 @@ user?.let {
it.userinfo()
it.logout()
}
-
-
-```
+```
\ No newline at end of file
diff --git a/davinci/build.gradle.kts b/davinci/build.gradle.kts
index 289f94f..debde3a 100644
--- a/davinci/build.gradle.kts
+++ b/davinci/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 Ping Identity. All rights reserved.
+ * Copyright (c) 2024 - 2025 Ping Identity. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
@@ -11,6 +11,7 @@ plugins {
id("com.pingidentity.convention.jacoco")
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinAndroid)
+ alias(libs.plugins.kotlinSerialization)
}
description = "DaVinci library"
@@ -49,6 +50,7 @@ dependencies {
testImplementation(project(":foundation:testrail"))
testImplementation(libs.kotlin.test)
+ testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.ktor.client.mock)
diff --git a/davinci/images/davinciSequence.png b/davinci/images/davinciSequence.png
deleted file mode 100644
index 916cad40f09f843db63dad490e44138cebd3bacc..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 105217
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