Skip to content

Commit 48f9872

Browse files
SaintPatrckclaude
andcommitted
Add testharness module for Credential Manager and Autofill e2e testing
Introduces a new standalone :testharness Android application module for end-to-end validation of Bitwarden's Credential Manager provider and Autofill framework implementations. Purpose: The test harness acts as a credential consumer client, allowing developers to trigger and verify all credential operations that Bitwarden supports as a provider without relying on external websites or applications. Supported Test Flows: - Password Creation: CreatePasswordRequest via Credential Manager - Password Retrieval: GetPasswordOption via Credential Manager - Passkey Creation: CreatePublicKeyCredentialRequest (FIDO2) - Passkey Authentication: GetPublicKeyCredentialOption (FIDO2) - Hybrid Operations: Combined password/passkey retrieval flows - Origin Parameter Testing: Privileged app simulation with custom origins - Autofill Framework: Placeholder for future autofill service testing Architecture: - MVVM + UDF: BaseViewModel with State/Action/Event patterns - Hilt DI: Full dependency injection throughout all layers - Jetpack Compose: Material 3 UI with BitwardenTheme integration - Navigation: Multi-screen architecture matching :app and :authenticator - Error Handling: Sealed Result classes, no exception-based handling Module Structure: - Activity layer: MainActivity with theme management - Navigation: RootNavScreen orchestrating all test flows - Data layer: CredentialTestManager wrapping AndroidX Credentials APIs - UI layer: Feature screens for each credential operation type - Test coverage: Comprehensive unit tests for all ViewModels and Screens Build Configuration: - Added :testharness to settings.gradle.kts - Integrated with detekt static analysis - Integrated with kover code coverage reporting - API 28+ requirement (AndroidX Credentials with Play Services) Requirements: - Android API 28+ - Bitwarden app installed as credential provider - Google Play Services (for API 28-33 compatibility) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7120eef commit 48f9872

File tree

66 files changed

+7026
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+7026
-0
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
kover(project(":cxf"))
2828
kover(project(":data"))
2929
kover(project(":network"))
30+
kover(project(":testharness"))
3031
kover(project(":ui"))
3132
}
3233

@@ -42,6 +43,7 @@ detekt {
4243
"cxf/src",
4344
"data/src",
4445
"network/src",
46+
"testharness/src",
4547
"ui/src",
4648
)
4749
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@ include(
5656
":cxf",
5757
":data",
5858
":network",
59+
":testharness",
5960
":ui",
6061
)

testharness/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/build
2+
*.iml
3+
.gradle
4+
/local.properties
5+
.idea/
6+
.DS_Store
7+
/captures
8+
.externalNativeBuild
9+
.cxx

testharness/README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Credential Manager Test Harness
2+
3+
## Purpose
4+
5+
This standalone application serves as a test client for validating the Bitwarden Android credential provider implementation. It uses the Android CredentialManager APIs to request credential operations and verify that the `:app` module responds correctly as a credential provider.
6+
7+
Future iterations will introduce validation for the Android Autofill Framework.
8+
9+
## Features
10+
11+
- **Password Creation**: Test `CreatePasswordRequest` flow through CredentialManager
12+
- **Password Retrieval**: Test `GetPasswordOption` flow through CredentialManager
13+
- **Passkey Creation**: Test `CreatePublicKeyCredentialRequest` flow through CredentialManager
14+
15+
## Requirements
16+
17+
- Android device or emulator running API 28+
18+
- Bitwarden app (`:app`) installed and configured as a credential provider
19+
- Google Play Services (for API 28-33 compatibility)
20+
21+
## Usage
22+
23+
### Setup
24+
25+
1. Build and install the main Bitwarden app (`:app`)
26+
2. Configure Bitwarden as a credential provider in system settings:
27+
- Settings → Passwords & accounts → Credential Manager
28+
- Enable Bitwarden as a provider
29+
3. Build and install the test harness:
30+
```bash
31+
./gradlew :testharness:installDebug
32+
```
33+
34+
### Running Tests
35+
36+
1. Launch "Credential Manager Test Harness" app
37+
2. Select a credential operation type (Password Create, Password Get, or Passkey Create)
38+
3. Fill in required fields:
39+
- **Password Create**: Username, Password, Origin (optional)
40+
- **Password Get**: No inputs required
41+
- **Passkey Create**: Username, Relying Party ID, Origin
42+
- **Passkey Get**: Relying Party ID, Origin
43+
4. Tap "Execute" button
44+
5. System credential picker should appear with Bitwarden as an option
45+
6. Select Bitwarden and follow the flow
46+
7. Result will be displayed in the app
47+
48+
## Known Limitations
49+
50+
1. **API 28-33 compatibility**: Requires Google Play Services for CredentialManager APIs on older Android versions.
51+
2. Passkey operations must be performed as a Privileged App due to security measures built in `:app`.
52+
53+
## Architecture
54+
55+
This module follows the same architectural patterns as the main Bitwarden app:
56+
- **MVVM + UDF**: `BaseViewModel` with State/Action/Event pattern
57+
- **Hilt DI**: Dependency injection throughout
58+
- **Compose UI**: Modern declarative UI with Material 3
59+
- **Result handling**: No exceptions, sealed result classes
60+
61+
## Testing
62+
63+
Run unit tests:
64+
```bash
65+
./gradlew :testharness:test
66+
```
67+
68+
### Debugging
69+
70+
Check Logcat for detailed error messages:
71+
```bash
72+
adb logcat | grep -E "CredentialManager|Bitwarden|TestHarness"
73+
```
74+
75+
## References
76+
77+
- [Android Credential Manager Documentation](https://developer.android.com/identity/credential-manager)
78+
- [Bitwarden Android Architecture](../docs/ARCHITECTURE.md)
79+
- [Passkey Registration Research](../PASSKEY_REGISTRATION_RESEARCH_REPORT.md)

testharness/build.gradle.kts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
3+
plugins {
4+
alias(libs.plugins.android.application)
5+
alias(libs.plugins.hilt)
6+
alias(libs.plugins.kotlin.android)
7+
alias(libs.plugins.kotlin.compose.compiler)
8+
alias(libs.plugins.kotlin.parcelize)
9+
alias(libs.plugins.kotlin.serialization)
10+
alias(libs.plugins.ksp)
11+
}
12+
13+
android {
14+
namespace = "com.x8bit.bitwarden.testharness"
15+
compileSdk = libs.versions.compileSdk.get().toInt()
16+
17+
defaultConfig {
18+
applicationId = "com.x8bit.bitwarden.testharness"
19+
// API 28 - CredentialManager with Play Services support
20+
minSdk = libs.versions.minSdkBwa.get().toInt()
21+
targetSdk = libs.versions.targetSdk.get().toInt()
22+
versionCode = 1
23+
versionName = "1.0.0"
24+
25+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
26+
}
27+
28+
buildTypes {
29+
debug {
30+
applicationIdSuffix = ".dev"
31+
isDebuggable = true
32+
}
33+
release {
34+
isMinifyEnabled = true
35+
proguardFiles(
36+
getDefaultProguardFile("proguard-android-optimize.txt"),
37+
"proguard-rules.pro",
38+
)
39+
}
40+
}
41+
42+
buildFeatures {
43+
compose = true
44+
buildConfig = true
45+
}
46+
47+
testOptions {
48+
// Required for Android framework classes in unit tests
49+
unitTests.isIncludeAndroidResources = true
50+
unitTests.isReturnDefaultValues = true
51+
}
52+
53+
compileOptions {
54+
sourceCompatibility = JavaVersion.valueOf(
55+
libs.versions.jvmTarget.get().replace(".", "_").let { "VERSION_$it" },
56+
)
57+
targetCompatibility = JavaVersion.valueOf(
58+
libs.versions.jvmTarget.get().replace(".", "_").let { "VERSION_$it" },
59+
)
60+
}
61+
62+
kotlin {
63+
compilerOptions {
64+
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
65+
}
66+
}
67+
}
68+
69+
dependencies {
70+
// Internal modules
71+
implementation(project(":annotation"))
72+
implementation(project(":core"))
73+
implementation(project(":ui"))
74+
75+
// AndroidX Credentials - PRIMARY DEPENDENCY
76+
implementation(libs.androidx.credentials)
77+
78+
// Hilt DI
79+
implementation(libs.google.hilt.android)
80+
ksp(libs.google.hilt.compiler)
81+
implementation(libs.androidx.hilt.navigation.compose)
82+
83+
// Compose UI
84+
implementation(platform(libs.androidx.compose.bom))
85+
implementation(libs.androidx.compose.material3)
86+
implementation(libs.androidx.compose.runtime)
87+
implementation(libs.androidx.compose.ui)
88+
implementation(libs.androidx.compose.ui.tooling.preview)
89+
implementation(libs.androidx.activity.compose)
90+
implementation(libs.androidx.lifecycle.runtime.compose)
91+
implementation(libs.androidx.splashscreen)
92+
93+
// Kotlin essentials
94+
implementation(libs.androidx.core.ktx)
95+
implementation(libs.kotlinx.coroutines.android)
96+
implementation(libs.kotlinx.serialization)
97+
implementation(libs.kotlinx.collections.immutable)
98+
99+
// Testing
100+
testImplementation(libs.androidx.compose.ui.test)
101+
testImplementation(libs.google.hilt.android.testing)
102+
testImplementation(platform(libs.junit.bom))
103+
testRuntimeOnly(libs.junit.platform.launcher)
104+
testImplementation(libs.junit.jupiter)
105+
testImplementation(libs.junit.vintage)
106+
testImplementation(libs.kotlinx.coroutines.test)
107+
testImplementation(libs.mockk.mockk)
108+
// Robolectric reserved for future Android framework tests if needed
109+
testImplementation(libs.robolectric.robolectric)
110+
testImplementation(libs.square.turbine)
111+
112+
testImplementation(testFixtures(project(":ui")))
113+
}
114+
115+
tasks {
116+
withType<Test> {
117+
useJUnitPlatform()
118+
maxHeapSize = "2g"
119+
maxParallelForks = Runtime.getRuntime().availableProcessors()
120+
}
121+
}

testharness/proguard-rules.pro

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# CredentialManager classes
2+
-keep class androidx.credentials.** { *; }
3+
-keep interface androidx.credentials.** { *; }
4+
5+
# Hilt
6+
-dontwarn com.google.errorprone.annotations.**
7+
8+
# Kotlinx Serialization
9+
-keepattributes *Annotation*, InnerClasses
10+
-dontnote kotlinx.serialization.AnnotationsKt
11+
-keepclassmembers class kotlinx.serialization.json.** {
12+
*** Companion;
13+
}
14+
-keepclasseswithmembers class kotlinx.serialization.json.** {
15+
kotlinx.serialization.KSerializer serializer(...);
16+
}
17+
-keep,includedescriptorclasses class com.x8bit.bitwarden.testharness.**$$serializer { *; }
18+
-keepclassmembers class com.x8bit.bitwarden.testharness.** {
19+
*** Companion;
20+
}
21+
-keepclasseswithmembers class com.x8bit.bitwarden.testharness.** {
22+
kotlinx.serialization.KSerializer serializer(...);
23+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.CREDENTIAL_MANAGER_SET_ORIGIN" />
5+
6+
<application
7+
android:name=".TestHarnessApplication"
8+
android:allowBackup="false"
9+
android:icon="@mipmap/ic_launcher"
10+
android:label="@string/app_name"
11+
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
12+
13+
<!-- Digital Asset Links for Credential Manager (empty for test consumer) -->
14+
<meta-data
15+
android:name="asset_statements"
16+
android:resource="@string/asset_statements" />
17+
18+
<activity
19+
android:name=".MainActivity"
20+
android:exported="true"
21+
android:windowSoftInputMode="adjustResize">
22+
<intent-filter>
23+
<action android:name="android.intent.action.MAIN" />
24+
<category android:name="android.intent.category.LAUNCHER" />
25+
</intent-filter>
26+
</activity>
27+
</application>
28+
29+
</manifest>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.x8bit.bitwarden.testharness
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.viewModels
7+
import androidx.compose.runtime.getValue
8+
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
9+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
10+
import androidx.navigation.compose.NavHost
11+
import androidx.navigation.compose.rememberNavController
12+
import com.bitwarden.ui.platform.theme.BitwardenTheme
13+
import com.bitwarden.ui.platform.util.setupEdgeToEdge
14+
import com.x8bit.bitwarden.testharness.ui.platform.feature.rootnav.RootNavScreen
15+
import com.x8bit.bitwarden.testharness.ui.platform.feature.rootnav.RootNavigationRoute
16+
import com.x8bit.bitwarden.testharness.ui.platform.feature.rootnav.rootNavDestination
17+
import dagger.hilt.android.AndroidEntryPoint
18+
import kotlinx.coroutines.flow.map
19+
20+
/**
21+
* Primary entry point for the Credential Manager test harness application.
22+
*
23+
* Delegates navigation to [RootNavScreen] following the same pattern as @app and @authenticator
24+
* modules. Handles Activity-level concerns like theme and splash screen.
25+
*
26+
* The root navigation is managed by [RootNavScreen] which orchestrates all test screen flows:
27+
* - Landing screen with test category selection (Autofill, Credential Manager)
28+
* - Individual test screens for each Credential Manager API operation
29+
*/
30+
@AndroidEntryPoint
31+
class MainActivity : ComponentActivity() {
32+
33+
private val mainViewModel: MainViewModel by viewModels()
34+
35+
override fun onCreate(savedInstanceState: Bundle?) {
36+
var shouldShowSplashScreen = true
37+
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
38+
super.onCreate(savedInstanceState)
39+
40+
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
41+
42+
setContent {
43+
val navController = rememberNavController()
44+
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
45+
46+
BitwardenTheme(
47+
theme = state.theme,
48+
) {
49+
NavHost(
50+
navController = navController,
51+
startDestination = RootNavigationRoute,
52+
) {
53+
rootNavDestination { shouldShowSplashScreen = false }
54+
}
55+
}
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)