mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 02:53:19 -05:00
Merge branch 'PM-37255/fill-assist-network-layer' into PM-37255/fill-assist-data-layer
This commit is contained in:
22 files changed
+118
-515
No files matched your search
+6
@@ -27,4 +27,10 @@ interface EnvironmentDiskSource {
|
||||
* Stores the [urls] for the given [userEmail].
|
||||
*/
|
||||
fun storePreAuthEnvironmentUrlDataForEmail(userEmail: String, urls: EnvironmentUrlDataJson)
|
||||
|
||||
/**
|
||||
* The fill-assist URL provided by the server config, or `null` if the server does not
|
||||
* configure fill-assist targeting rules.
|
||||
*/
|
||||
var fillAssistRulesUrl: String?
|
||||
}
|
||||
+5
@@ -11,6 +11,7 @@ import kotlinx.serialization.json.Json
|
||||
|
||||
private const val PRE_AUTH_URLS_KEY = "preAuthEnvironmentUrls"
|
||||
private const val EMAIL_VERIFICATION_URLS = "emailVerificationUrls"
|
||||
private const val FILL_ASSIST_RULES_URL_KEY = "fillAssistRulesUrl"
|
||||
|
||||
/**
|
||||
* Primary implementation of [EnvironmentDiskSource].
|
||||
@@ -54,4 +55,8 @@ class EnvironmentDiskSourceImpl(
|
||||
value = json.encodeToString(urls),
|
||||
)
|
||||
}
|
||||
|
||||
override var fillAssistRulesUrl: String?
|
||||
get() = getString(key = FILL_ASSIST_RULES_URL_KEY)
|
||||
set(value) = putString(key = FILL_ASSIST_RULES_URL_KEY, value = value)
|
||||
}
|
||||
@@ -31,4 +31,6 @@ class BaseUrlsProviderImpl(
|
||||
.toEnvironmentUrlsOrDefault()
|
||||
.environmentUrlData
|
||||
.baseEventsUrl
|
||||
|
||||
override fun getBaseFillAssistUrl(): String? = environmentDiskSource.fillAssistRulesUrl
|
||||
}
|
||||
+15
@@ -95,11 +95,26 @@ class EnvironmentDiskSourceTest {
|
||||
json.parseToJsonElement(requireNotNull(actual)),
|
||||
)
|
||||
}
|
||||
@Test
|
||||
fun `fillAssistRulesUrl should pull from and update SharedPreferences`() {
|
||||
assertNull(environmentDiskSource.fillAssistRulesUrl)
|
||||
assertNull(fakeSharedPreferences.getString(FILL_ASSIST_RULES_URL_KEY, null))
|
||||
|
||||
environmentDiskSource.fillAssistRulesUrl = "https://fill-assist.example.com/"
|
||||
assertEquals(
|
||||
"https://fill-assist.example.com/",
|
||||
fakeSharedPreferences.getString(FILL_ASSIST_RULES_URL_KEY, null),
|
||||
)
|
||||
|
||||
environmentDiskSource.fillAssistRulesUrl = null
|
||||
assertNull(fakeSharedPreferences.getString(FILL_ASSIST_RULES_URL_KEY, null))
|
||||
}
|
||||
}
|
||||
|
||||
private const val EMAIL = "email@example.com"
|
||||
private const val EMAIL_VERIFICATION_URLS_KEY = "bwPreferencesStorage:emailVerificationUrls"
|
||||
private const val PRE_AUTH_URLS_KEY = "bwPreferencesStorage:preAuthEnvironmentUrls"
|
||||
private const val FILL_ASSIST_RULES_URL_KEY = "bwPreferencesStorage:fillAssistRulesUrl"
|
||||
|
||||
private const val ENVIRONMENT_URL_DATA_JSON = """
|
||||
{
|
||||
|
||||
+2
@@ -29,6 +29,8 @@ class FakeEnvironmentDiskSource : EnvironmentDiskSource {
|
||||
storedEmailVerificationUrls[userEmail] = urls
|
||||
}
|
||||
|
||||
override var fillAssistRulesUrl: String? = null
|
||||
|
||||
private val mutablePreAuthEnvironmentUrlDataFlow =
|
||||
bufferedMutableSharedFlow<EnvironmentUrlDataJson?>(replay = 1)
|
||||
}
|
||||
+13
@@ -4,6 +4,7 @@ import com.bitwarden.data.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.FakeEnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.provider.BaseUrlsProviderImpl
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BaseUrlsProviderTest {
|
||||
@@ -66,4 +67,16 @@ class BaseUrlsProviderTest {
|
||||
baseUrlsManager.getBaseEventsUrl(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBaseFillAssistUrl should return url from disk source when present`() {
|
||||
fakeEnvironmentDiskSource.fillAssistRulesUrl = "https://example.com/"
|
||||
assertEquals("https://example.com/", baseUrlsManager.getBaseFillAssistUrl())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBaseFillAssistUrl should return null when not set`() {
|
||||
fakeEnvironmentDiskSource.fillAssistRulesUrl = null
|
||||
assertNull(baseUrlsManager.getBaseFillAssistUrl())
|
||||
}
|
||||
}
|
||||
+1
@@ -36,6 +36,7 @@ class SdkRepositoryFactoryTests {
|
||||
override fun getBaseApiUrl(): String = BASE_API_URL
|
||||
override fun getBaseIdentityUrl(): String = BASE_IDENTITY_URL
|
||||
override fun getBaseEventsUrl(): String = BASE_EVENTS_URL
|
||||
override fun getBaseFillAssistUrl(): String? = null
|
||||
},
|
||||
authTokenProvider = mockk(),
|
||||
certificateProvider = mockk(),
|
||||
|
||||
+2
@@ -20,4 +20,6 @@ object BaseUrlsProviderImpl : BaseUrlsProvider {
|
||||
|
||||
override fun getBaseEventsUrl(): String =
|
||||
Environment.Us.environmentUrlData.baseEventsUrl
|
||||
|
||||
override fun getBaseFillAssistUrl(): String? = null
|
||||
}
|
||||
@@ -158,9 +158,7 @@ internal class BitwardenServiceClientImpl(
|
||||
}
|
||||
|
||||
override val fillAssistService: FillAssistService by lazy {
|
||||
FillAssistServiceImpl(
|
||||
api = retrofits.createStaticRetrofit().create(),
|
||||
)
|
||||
FillAssistServiceImpl(api = retrofits.fillAssistRetrofit.create())
|
||||
}
|
||||
|
||||
override val haveIBeenPwnedService: HaveIBeenPwnedService by lazy {
|
||||
|
||||
@@ -4,26 +4,24 @@ import com.bitwarden.network.model.FillAssistFormsJson
|
||||
import com.bitwarden.network.model.FillAssistManifestJson
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* Defines endpoints for retrieving fill-assist targeting rules from the fill-assist service.
|
||||
* Uses [Url] to support the dynamic base URL provided by server config at runtime.
|
||||
* Defines endpoints for retrieving fill-assist targeting rules. The base URL is set dynamically
|
||||
* at runtime via [com.bitwarden.network.interceptor.BaseUrlInterceptors.fillAssistInterceptor].
|
||||
*/
|
||||
internal interface FillAssistApi {
|
||||
/**
|
||||
* Fetches the fill-assist manifest from the given [url].
|
||||
* Fetches the fill-assist manifest.
|
||||
*/
|
||||
@GET
|
||||
suspend fun getManifest(
|
||||
@Url url: String,
|
||||
): NetworkResult<FillAssistManifestJson>
|
||||
@GET("manifest.json")
|
||||
suspend fun getManifest(): NetworkResult<FillAssistManifestJson>
|
||||
|
||||
/**
|
||||
* Fetches and decodes the forms rules file from [url].
|
||||
* Fetches the forms rules file by [filename] (e.g. "forms.v1.json").
|
||||
*/
|
||||
@GET
|
||||
@GET("{filename}")
|
||||
suspend fun getForms(
|
||||
@Url url: String,
|
||||
@Path("filename") filename: String,
|
||||
): NetworkResult<FillAssistFormsJson>
|
||||
}
|
||||
@@ -29,4 +29,11 @@ internal class BaseUrlInterceptors(
|
||||
val eventsInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
|
||||
baseUrlsProvider.getBaseEventsUrl()
|
||||
}
|
||||
|
||||
/**
|
||||
* An interceptor for fill-assist calls.
|
||||
*/
|
||||
val fillAssistInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
|
||||
baseUrlsProvider.getBaseFillAssistUrl()
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,10 @@ interface BaseUrlsProvider {
|
||||
* Gets the base URL for "/events" calls.
|
||||
*/
|
||||
fun getBaseEventsUrl(): String
|
||||
|
||||
/**
|
||||
* Gets the base URL for fill-assist calls, or null if the server does not provide
|
||||
* fill-assist targeting rules.
|
||||
*/
|
||||
fun getBaseFillAssistUrl(): String?
|
||||
}
|
||||
@@ -62,6 +62,8 @@ data class ConfigResponseJson(
|
||||
* @param identityUrl The URL of the identity service in the environment.
|
||||
* @param notificationsUrl The URL of the notifications service in the environment.
|
||||
* @param ssoUrl The URL of the single sign-on (SSO) service in the environment.
|
||||
* @param fillAssistRulesUrl The base URL of the fill-assist targeting rules, or null if
|
||||
* the server does not provide fill-assist rules.
|
||||
*/
|
||||
@Serializable
|
||||
data class EnvironmentJson(
|
||||
|
||||
@@ -14,10 +14,10 @@ import kotlinx.serialization.json.JsonElement
|
||||
@Serializable
|
||||
data class FillAssistFormsJson(
|
||||
@SerialName("schemaVersion")
|
||||
val schemaVersion: String? = null,
|
||||
val schemaVersion: String,
|
||||
|
||||
@SerialName("hosts")
|
||||
val hosts: Map<String, HostEntryJson?>? = null,
|
||||
val hosts: Map<String, HostEntryJson?>,
|
||||
) {
|
||||
/**
|
||||
* Form descriptions and pathname-specific overrides for a single host.
|
||||
@@ -28,10 +28,10 @@ data class FillAssistFormsJson(
|
||||
@Serializable
|
||||
data class HostEntryJson(
|
||||
@SerialName("forms")
|
||||
val forms: List<FormJson>? = null,
|
||||
val forms: List<FormJson>?,
|
||||
|
||||
@SerialName("pathnames")
|
||||
val pathnames: Map<String, PathnameEntryJson?>? = null,
|
||||
val pathnames: Map<String, PathnameEntryJson?>?,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ data class FillAssistFormsJson(
|
||||
@Serializable
|
||||
data class PathnameEntryJson(
|
||||
@SerialName("forms")
|
||||
val forms: List<FormJson>? = null,
|
||||
val forms: List<FormJson>,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -57,12 +57,12 @@ data class FillAssistFormsJson(
|
||||
@Serializable
|
||||
data class FormJson(
|
||||
@SerialName("category")
|
||||
val category: String? = null,
|
||||
val category: String,
|
||||
|
||||
@SerialName("container")
|
||||
val container: List<String>? = null,
|
||||
val container: List<String>?,
|
||||
|
||||
@SerialName("fields")
|
||||
val fields: Map<String, JsonElement>? = null,
|
||||
val fields: Map<String, JsonElement>,
|
||||
)
|
||||
}
|
||||
@@ -14,16 +14,16 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class FillAssistManifestJson(
|
||||
@SerialName("buildId")
|
||||
val buildId: String? = null,
|
||||
val buildId: String,
|
||||
|
||||
@SerialName("timestamp")
|
||||
val timestamp: String? = null,
|
||||
val timestamp: String,
|
||||
|
||||
@SerialName("gitSha")
|
||||
val gitSha: String? = null,
|
||||
val gitSha: String,
|
||||
|
||||
@SerialName("maps")
|
||||
val maps: MapsJson? = null,
|
||||
val maps: MapsJson,
|
||||
) {
|
||||
/**
|
||||
* Container for all available maps.
|
||||
@@ -34,13 +34,13 @@ data class FillAssistManifestJson(
|
||||
@Serializable
|
||||
data class MapsJson(
|
||||
@SerialName("forms")
|
||||
val forms: Map<String, FileEntryJson?>?,
|
||||
val forms: Map<String, FileEntryJson>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Metadata for a single versioned file in a map.
|
||||
*
|
||||
* @property filename The filename to fetch (e.g. "forms.v0.json").
|
||||
* @property filename The filename to fetch (e.g. "forms.v1.json").
|
||||
* @property cid The SHA-256 content hash in "sha256:<hex>" format. Used as a staleness key
|
||||
* to detect when the forms file has changed on the server, avoiding unnecessary re-downloads.
|
||||
* @property schema The schema filename associated with this file version.
|
||||
@@ -50,15 +50,15 @@ data class FillAssistManifestJson(
|
||||
@Serializable
|
||||
data class FileEntryJson(
|
||||
@SerialName("filename")
|
||||
val filename: String? = null,
|
||||
val filename: String,
|
||||
|
||||
@SerialName("cid")
|
||||
val cid: String? = null,
|
||||
val cid: String,
|
||||
|
||||
@SerialName("schema")
|
||||
val schema: String? = null,
|
||||
val schema: String,
|
||||
|
||||
@SerialName("deprecated")
|
||||
val deprecated: Boolean? = null,
|
||||
val deprecated: Boolean?,
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,12 @@ internal interface Retrofits {
|
||||
*/
|
||||
val unauthenticatedIdentityRetrofit: Retrofit
|
||||
|
||||
/**
|
||||
* Allows access to fill-assist calls. The base URL is determined dynamically via the
|
||||
* [BaseUrlInterceptors.fillAssistInterceptor].
|
||||
*/
|
||||
val fillAssistRetrofit: Retrofit
|
||||
|
||||
/**
|
||||
* Allows access to static API calls (ex: external APIs).
|
||||
*
|
||||
|
||||
@@ -64,6 +64,16 @@ internal class RetrofitsImpl(
|
||||
|
||||
//endregion Unauthenticated Retrofits
|
||||
|
||||
//region Fill-Assist Retrofit
|
||||
|
||||
override val fillAssistRetrofit: Retrofit by lazy {
|
||||
createUnauthenticatedRetrofit(
|
||||
baseUrlInterceptor = baseUrlInterceptors.fillAssistInterceptor,
|
||||
)
|
||||
}
|
||||
|
||||
//endregion Fill-Assist Retrofit
|
||||
|
||||
//region Static Retrofit
|
||||
|
||||
override fun createStaticRetrofit(isAuthenticated: Boolean, baseUrl: String): Retrofit {
|
||||
|
||||
@@ -8,15 +8,14 @@ import com.bitwarden.network.model.FillAssistManifestJson
|
||||
*/
|
||||
interface FillAssistService {
|
||||
/**
|
||||
* Fetches and parses the fill-assist manifest from [url].
|
||||
* Fetches and parses the fill-assist manifest.
|
||||
*/
|
||||
suspend fun getManifest(url: String): Result<FillAssistManifestJson>
|
||||
suspend fun getManifest(): Result<FillAssistManifestJson>
|
||||
|
||||
/**
|
||||
* Downloads and parses the forms rules file from [formsUrl].
|
||||
* Downloads and parses the forms rules file identified by [filename] (e.g. "forms.v1.json").
|
||||
*
|
||||
* Returns [Result.failure] if the network request fails or parsing fails.
|
||||
* Version-agnostic: any forms file URL can be passed regardless of schema version.
|
||||
*/
|
||||
suspend fun getForms(formsUrl: String): Result<FillAssistFormsJson>
|
||||
suspend fun getForms(filename: String): Result<FillAssistFormsJson>
|
||||
}
|
||||
@@ -12,9 +12,9 @@ internal class FillAssistServiceImpl(
|
||||
private val api: FillAssistApi,
|
||||
) : FillAssistService {
|
||||
|
||||
override suspend fun getManifest(url: String): Result<FillAssistManifestJson> =
|
||||
api.getManifest(url = url).toResult()
|
||||
override suspend fun getManifest(): Result<FillAssistManifestJson> =
|
||||
api.getManifest().toResult()
|
||||
|
||||
override suspend fun getForms(formsUrl: String): Result<FillAssistFormsJson> =
|
||||
api.getForms(url = formsUrl).toResult()
|
||||
override suspend fun getForms(filename: String): Result<FillAssistFormsJson> =
|
||||
api.getForms(filename = filename).toResult()
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FillAssistFormsJsonTest {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Test
|
||||
fun `deserialize simple login form`() {
|
||||
assertEquals(
|
||||
SIMPLE_LOGIN_FORMS,
|
||||
json.decodeFromString<FillAssistFormsJson>(SIMPLE_LOGIN_JSON),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize host with null value is excluded host`() {
|
||||
val result = json.decodeFromString<FillAssistFormsJson>(NULL_HOST_JSON)
|
||||
assertNull(result.hosts?.get("excluded.com"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize null pathname is excluded path`() {
|
||||
val result = json.decodeFromString<FillAssistFormsJson>(NULL_PATHNAME_JSON)
|
||||
assertNull(result.hosts?.get("example.com")?.pathnames?.get("/excluded"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize composite OTP field`() {
|
||||
assertEquals(
|
||||
OTP_FORMS,
|
||||
json.decodeFromString<FillAssistFormsJson>(OTP_JSON),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize compound selector with multiple alternatives`() {
|
||||
assertEquals(
|
||||
HONEYPOT_FORMS,
|
||||
json.decodeFromString<FillAssistFormsJson>(HONEYPOT_JSON),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize form with container`() {
|
||||
assertEquals(
|
||||
CONTAINER_FORMS,
|
||||
json.decodeFromString<FillAssistFormsJson>(CONTAINER_JSON),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize form with actions field is graceful`() {
|
||||
// actions is intentionally excluded from FormJson — handled by ignoreUnknownKeys.
|
||||
assertEquals(
|
||||
SIMPLE_LOGIN_FORMS,
|
||||
json.decodeFromString<FillAssistFormsJson>(SIMPLE_LOGIN_WITH_ACTIONS_JSON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val SIMPLE_LOGIN_FORMS = FillAssistFormsJson(
|
||||
schemaVersion = "1.0.0",
|
||||
hosts = mapOf(
|
||||
"example.com" to FillAssistFormsJson.HostEntryJson(
|
||||
forms = listOf(
|
||||
FillAssistFormsJson.FormJson(
|
||||
category = "account-login",
|
||||
container = null,
|
||||
fields = mapOf(
|
||||
"username" to JsonArray(listOf(JsonPrimitive("input#username"))),
|
||||
"password" to JsonArray(listOf(JsonPrimitive("input#password"))),
|
||||
),
|
||||
),
|
||||
),
|
||||
pathnames = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val OTP_FORMS = FillAssistFormsJson(
|
||||
schemaVersion = "1.0.0",
|
||||
hosts = mapOf(
|
||||
"example.com" to FillAssistFormsJson.HostEntryJson(
|
||||
forms = null,
|
||||
pathnames = mapOf(
|
||||
"/login" to FillAssistFormsJson.PathnameEntryJson(
|
||||
forms = listOf(
|
||||
FillAssistFormsJson.FormJson(
|
||||
category = "account-login",
|
||||
container = null,
|
||||
fields = mapOf(
|
||||
"oneTimeCode" to JsonArray(
|
||||
listOf(
|
||||
JsonArray(
|
||||
listOf(
|
||||
JsonPrimitive("input[name='otp-0']"),
|
||||
JsonPrimitive("input[name='otp-1']"),
|
||||
JsonPrimitive("input[name='otp-2']"),
|
||||
JsonPrimitive("input[name='otp-3']"),
|
||||
JsonPrimitive("input[name='otp-4']"),
|
||||
JsonPrimitive("input[name='otp-5']"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val HONEYPOT_FORMS = FillAssistFormsJson(
|
||||
schemaVersion = "1.0.0",
|
||||
hosts = mapOf(
|
||||
"example.com" to FillAssistFormsJson.HostEntryJson(
|
||||
forms = listOf(
|
||||
FillAssistFormsJson.FormJson(
|
||||
category = "account-login",
|
||||
container = null,
|
||||
fields = mapOf(
|
||||
"username" to JsonArray(
|
||||
listOf(
|
||||
JsonPrimitive("input#password[name='password']"),
|
||||
JsonPrimitive("input[name='password']"),
|
||||
),
|
||||
),
|
||||
"password" to JsonArray(
|
||||
listOf(
|
||||
JsonPrimitive("input#username[name='username']"),
|
||||
JsonPrimitive("input[name='username']"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
pathnames = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val CONTAINER_FORMS = FillAssistFormsJson(
|
||||
schemaVersion = "1.0.0",
|
||||
hosts = mapOf(
|
||||
"example.com" to FillAssistFormsJson.HostEntryJson(
|
||||
forms = null,
|
||||
pathnames = mapOf(
|
||||
"/login" to FillAssistFormsJson.PathnameEntryJson(
|
||||
forms = listOf(
|
||||
FillAssistFormsJson.FormJson(
|
||||
category = "account-login",
|
||||
container = listOf("div#login-container"),
|
||||
fields = mapOf(
|
||||
"username" to JsonArray(listOf(JsonPrimitive("input#user"))),
|
||||
"password" to JsonArray(listOf(JsonPrimitive("input#pass"))),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private const val SIMPLE_LOGIN_JSON = """
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"forms": [
|
||||
{
|
||||
"category": "account-login",
|
||||
"fields": {
|
||||
"username": ["input#username"],
|
||||
"password": ["input#password"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private const val NULL_HOST_JSON = """
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"hosts": {
|
||||
"excluded.com": null
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private const val NULL_PATHNAME_JSON = """
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"pathnames": {
|
||||
"/excluded": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private const val OTP_JSON = """
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"pathnames": {
|
||||
"/login": {
|
||||
"forms": [
|
||||
{
|
||||
"category": "account-login",
|
||||
"fields": {
|
||||
"oneTimeCode": [
|
||||
["input[name='otp-0']","input[name='otp-1']","input[name='otp-2']",
|
||||
"input[name='otp-3']","input[name='otp-4']","input[name='otp-5']"]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private const val HONEYPOT_JSON = """
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"forms": [
|
||||
{
|
||||
"category": "account-login",
|
||||
"fields": {
|
||||
"username": ["input#password[name='password']", "input[name='password']"],
|
||||
"password": ["input#username[name='username']", "input[name='username']"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private const val CONTAINER_JSON = """
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"pathnames": {
|
||||
"/login": {
|
||||
"forms": [
|
||||
{
|
||||
"category": "account-login",
|
||||
"container": ["div#login-container"],
|
||||
"fields": {
|
||||
"username": ["input#user"],
|
||||
"password": ["input#pass"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
// Same structure as SIMPLE_LOGIN_JSON with the actions field present — verifies that
|
||||
// actions (intentionally omitted from FormJson) is handled by ignoreUnknownKeys = true.
|
||||
private const val SIMPLE_LOGIN_WITH_ACTIONS_JSON = """
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"hosts": {
|
||||
"example.com": {
|
||||
"forms": [
|
||||
{
|
||||
"category": "account-login",
|
||||
"fields": {
|
||||
"username": ["input#username"],
|
||||
"password": ["input#password"]
|
||||
},
|
||||
"actions": {
|
||||
"submit": ["button#submit"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -1,165 +0,0 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FillAssistManifestJsonTest {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Test
|
||||
fun `deserialize full manifest with v0 entry`() {
|
||||
assertEquals(
|
||||
FULL_MANIFEST,
|
||||
json.decodeFromString<FillAssistManifestJson>(MANIFEST_JSON),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize manifest with multiple version entries`() {
|
||||
assertEquals(
|
||||
MULTI_VERSION_MANIFEST,
|
||||
json.decodeFromString<FillAssistManifestJson>(MANIFEST_MULTI_VERSION_JSON),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize manifest with unknown top-level fields is graceful`() {
|
||||
assertEquals(
|
||||
FULL_MANIFEST,
|
||||
json.decodeFromString<FillAssistManifestJson>(MANIFEST_EXTRA_FIELDS_JSON),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize minimal manifest with null fields`() {
|
||||
val result = json.decodeFromString<FillAssistManifestJson>("{}")
|
||||
assertNull(result.buildId)
|
||||
assertNull(result.maps)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize manifest with deprecated version entry`() {
|
||||
assertEquals(
|
||||
DEPRECATED_MANIFEST,
|
||||
json.decodeFromString<FillAssistManifestJson>(MANIFEST_DEPRECATED_JSON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val FULL_MANIFEST = FillAssistManifestJson(
|
||||
buildId = "local-build",
|
||||
timestamp = "2026-05-20T15:01:02.956Z",
|
||||
gitSha = "abc123",
|
||||
maps = FillAssistManifestJson.MapsJson(
|
||||
forms = mapOf(
|
||||
"v0" to FillAssistManifestJson.FileEntryJson(
|
||||
filename = "forms.v0.json",
|
||||
cid = "sha256:abc123def456",
|
||||
schema = "forms.v0.schema.json",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val MULTI_VERSION_MANIFEST = FillAssistManifestJson(
|
||||
buildId = "local-build",
|
||||
timestamp = null,
|
||||
gitSha = null,
|
||||
maps = FillAssistManifestJson.MapsJson(
|
||||
forms = mapOf(
|
||||
"v0" to FillAssistManifestJson.FileEntryJson(
|
||||
filename = "forms.v0.json",
|
||||
cid = "sha256:aaa",
|
||||
schema = "forms.v0.schema.json",
|
||||
),
|
||||
"v1" to FillAssistManifestJson.FileEntryJson(
|
||||
filename = "forms.v1.json",
|
||||
cid = "sha256:bbb",
|
||||
schema = "forms.v1.schema.json",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private const val MANIFEST_JSON = """
|
||||
{
|
||||
"buildId": "local-build",
|
||||
"timestamp": "2026-05-20T15:01:02.956Z",
|
||||
"gitSha": "abc123",
|
||||
"maps": {
|
||||
"forms": {
|
||||
"v0": {
|
||||
"filename": "forms.v0.json",
|
||||
"cid": "sha256:abc123def456",
|
||||
"schema": "forms.v0.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private const val MANIFEST_MULTI_VERSION_JSON = """
|
||||
{
|
||||
"buildId": "local-build",
|
||||
"maps": {
|
||||
"forms": {
|
||||
"v0": { "filename": "forms.v0.json", "cid": "sha256:aaa", "schema": "forms.v0.schema.json" },
|
||||
"v1": { "filename": "forms.v1.json", "cid": "sha256:bbb", "schema": "forms.v1.schema.json" }
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private val DEPRECATED_MANIFEST = FillAssistManifestJson(
|
||||
buildId = "local-build",
|
||||
timestamp = null,
|
||||
gitSha = null,
|
||||
maps = FillAssistManifestJson.MapsJson(
|
||||
forms = mapOf(
|
||||
"v0" to FillAssistManifestJson.FileEntryJson(
|
||||
filename = "forms.v0.json",
|
||||
cid = "sha256:abc123def456",
|
||||
schema = "forms.v0.schema.json",
|
||||
deprecated = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private const val MANIFEST_DEPRECATED_JSON = """
|
||||
{
|
||||
"buildId": "local-build",
|
||||
"maps": {
|
||||
"forms": {
|
||||
"v0": {
|
||||
"filename": "forms.v0.json",
|
||||
"cid": "sha256:abc123def456",
|
||||
"schema": "forms.v0.schema.json",
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
// Same structure as MANIFEST_JSON with an extra unknown key — verifies ignoreUnknownKeys = true.
|
||||
private const val MANIFEST_EXTRA_FIELDS_JSON = """
|
||||
{
|
||||
"buildId": "local-build",
|
||||
"timestamp": "2026-05-20T15:01:02.956Z",
|
||||
"gitSha": "abc123",
|
||||
"checksums": "ignored",
|
||||
"maps": {
|
||||
"forms": {
|
||||
"v0": {
|
||||
"filename": "forms.v0.json",
|
||||
"cid": "sha256:abc123def456",
|
||||
"schema": "forms.v0.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -22,25 +22,25 @@ class FillAssistServiceTest : BaseServiceTest() {
|
||||
@Test
|
||||
fun `getManifest should parse manifest response`() = runTest {
|
||||
server.enqueue(MockResponse().setBody(MANIFEST_JSON))
|
||||
assertEquals(MANIFEST.asSuccess(), service.getManifest(url = "$urlPrefix/manifest.json"))
|
||||
assertEquals(MANIFEST.asSuccess(), service.getManifest())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getManifest should return failure on server error`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(500))
|
||||
assertTrue(service.getManifest(url = "$urlPrefix/manifest.json").isFailure)
|
||||
assertTrue(service.getManifest().isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getForms should parse and return forms`() = runTest {
|
||||
server.enqueue(MockResponse().setBody(FORMS_V1_JSON))
|
||||
assertEquals(FORMS_V1.asSuccess(), service.getForms(formsUrl = "$urlPrefix/forms.v1.json"))
|
||||
assertEquals(FORMS_V1.asSuccess(), service.getForms(filename = "forms.v1.json"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getForms should return failure on server error`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(404))
|
||||
assertTrue(service.getForms(formsUrl = "$urlPrefix/forms.v1.json").isFailure)
|
||||
assertTrue(service.getForms(filename = "forms.v1.json").isFailure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ private val MANIFEST = FillAssistManifestJson(
|
||||
filename = "forms.v1.json",
|
||||
cid = "sha256:5b8f688d24bb9c38b4094838fa2baacb3cc4ab302e3545adf016b05f6b6b96db",
|
||||
schema = "forms.v1.schema.json",
|
||||
deprecated = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user