diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSource.kt index 85f0fb8dcf..af8bf6f863 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSource.kt @@ -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? } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceImpl.kt index 7a5a531344..c3c8b43585 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceImpl.kt @@ -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) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/provider/BaseUrlsProviderImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/provider/BaseUrlsProviderImpl.kt index 763279524b..28fc40d443 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/provider/BaseUrlsProviderImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/provider/BaseUrlsProviderImpl.kt @@ -31,4 +31,6 @@ class BaseUrlsProviderImpl( .toEnvironmentUrlsOrDefault() .environmentUrlData .baseEventsUrl + + override fun getBaseFillAssistUrl(): String? = environmentDiskSource.fillAssistRulesUrl } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceTest.kt index 9aab10046a..333b440543 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceTest.kt @@ -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 = """ { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/FakeEnvironmentDiskSource.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/FakeEnvironmentDiskSource.kt index f3eda792ea..c37f934d5d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/FakeEnvironmentDiskSource.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/FakeEnvironmentDiskSource.kt @@ -29,6 +29,8 @@ class FakeEnvironmentDiskSource : EnvironmentDiskSource { storedEmailVerificationUrls[userEmail] = urls } + override var fillAssistRulesUrl: String? = null + private val mutablePreAuthEnvironmentUrlDataFlow = bufferedMutableSharedFlow(replay = 1) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/provider/BaseUrlsProviderTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/provider/BaseUrlsProviderTest.kt index 811236105b..598ddbc549 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/provider/BaseUrlsProviderTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/provider/BaseUrlsProviderTest.kt @@ -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()) + } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt index a577cdadf4..62b2cf1027 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryTests.kt @@ -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(), diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/provider/BaseUrlsProviderImpl.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/provider/BaseUrlsProviderImpl.kt index 5105f38003..aaf3fb706f 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/provider/BaseUrlsProviderImpl.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/provider/BaseUrlsProviderImpl.kt @@ -20,4 +20,6 @@ object BaseUrlsProviderImpl : BaseUrlsProvider { override fun getBaseEventsUrl(): String = Environment.Us.environmentUrlData.baseEventsUrl + + override fun getBaseFillAssistUrl(): String? = null } diff --git a/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt b/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt index 7afab54ab6..dd138ed848 100644 --- a/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt @@ -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 { diff --git a/network/src/main/kotlin/com/bitwarden/network/api/FillAssistApi.kt b/network/src/main/kotlin/com/bitwarden/network/api/FillAssistApi.kt index 0ca1c93d23..280f0afc90 100644 --- a/network/src/main/kotlin/com/bitwarden/network/api/FillAssistApi.kt +++ b/network/src/main/kotlin/com/bitwarden/network/api/FillAssistApi.kt @@ -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 + @GET("manifest.json") + suspend fun getManifest(): NetworkResult /** - * 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 } diff --git a/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt b/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt index dceaf9f5e5..5413395b98 100644 --- a/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt +++ b/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt @@ -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() + } } diff --git a/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlsProvider.kt b/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlsProvider.kt index 9cbca4d38d..4dd0e6f80b 100644 --- a/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlsProvider.kt +++ b/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlsProvider.kt @@ -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? } diff --git a/network/src/main/kotlin/com/bitwarden/network/model/ConfigResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/ConfigResponseJson.kt index 834ab78bf3..eb2d7dbea7 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/ConfigResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/ConfigResponseJson.kt @@ -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( diff --git a/network/src/main/kotlin/com/bitwarden/network/model/FillAssistFormsJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/FillAssistFormsJson.kt index 9f56cca659..a4b468482c 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/FillAssistFormsJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/FillAssistFormsJson.kt @@ -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? = null, + val hosts: Map, ) { /** * 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? = null, + val forms: List?, @SerialName("pathnames") - val pathnames: Map? = null, + val pathnames: Map?, ) /** @@ -42,7 +42,7 @@ data class FillAssistFormsJson( @Serializable data class PathnameEntryJson( @SerialName("forms") - val forms: List? = null, + val forms: List, ) /** @@ -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? = null, + val container: List?, @SerialName("fields") - val fields: Map? = null, + val fields: Map, ) } diff --git a/network/src/main/kotlin/com/bitwarden/network/model/FillAssistManifestJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/FillAssistManifestJson.kt index 460039ff03..3cc73b1dd3 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/FillAssistManifestJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/FillAssistManifestJson.kt @@ -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?, + val forms: Map, ) /** * 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:" 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?, ) } diff --git a/network/src/main/kotlin/com/bitwarden/network/retrofit/Retrofits.kt b/network/src/main/kotlin/com/bitwarden/network/retrofit/Retrofits.kt index 002ad3e4a6..e0ba3d4373 100644 --- a/network/src/main/kotlin/com/bitwarden/network/retrofit/Retrofits.kt +++ b/network/src/main/kotlin/com/bitwarden/network/retrofit/Retrofits.kt @@ -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). * diff --git a/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt b/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt index 227e7695e7..525cad6b96 100644 --- a/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt @@ -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 { diff --git a/network/src/main/kotlin/com/bitwarden/network/service/FillAssistService.kt b/network/src/main/kotlin/com/bitwarden/network/service/FillAssistService.kt index 007a25f13d..59c07a4dc6 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/FillAssistService.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/FillAssistService.kt @@ -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 + suspend fun getManifest(): Result /** - * 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 + suspend fun getForms(filename: String): Result } diff --git a/network/src/main/kotlin/com/bitwarden/network/service/FillAssistServiceImpl.kt b/network/src/main/kotlin/com/bitwarden/network/service/FillAssistServiceImpl.kt index 7668e4b346..aa4b848662 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/FillAssistServiceImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/FillAssistServiceImpl.kt @@ -12,9 +12,9 @@ internal class FillAssistServiceImpl( private val api: FillAssistApi, ) : FillAssistService { - override suspend fun getManifest(url: String): Result = - api.getManifest(url = url).toResult() + override suspend fun getManifest(): Result = + api.getManifest().toResult() - override suspend fun getForms(formsUrl: String): Result = - api.getForms(url = formsUrl).toResult() + override suspend fun getForms(filename: String): Result = + api.getForms(filename = filename).toResult() } diff --git a/network/src/test/kotlin/com/bitwarden/network/model/FillAssistFormsJsonTest.kt b/network/src/test/kotlin/com/bitwarden/network/model/FillAssistFormsJsonTest.kt deleted file mode 100644 index 532154fbb9..0000000000 --- a/network/src/test/kotlin/com/bitwarden/network/model/FillAssistFormsJsonTest.kt +++ /dev/null @@ -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(SIMPLE_LOGIN_JSON), - ) - } - - @Test - fun `deserialize host with null value is excluded host`() { - val result = json.decodeFromString(NULL_HOST_JSON) - assertNull(result.hosts?.get("excluded.com")) - } - - @Test - fun `deserialize null pathname is excluded path`() { - val result = json.decodeFromString(NULL_PATHNAME_JSON) - assertNull(result.hosts?.get("example.com")?.pathnames?.get("/excluded")) - } - - @Test - fun `deserialize composite OTP field`() { - assertEquals( - OTP_FORMS, - json.decodeFromString(OTP_JSON), - ) - } - - @Test - fun `deserialize compound selector with multiple alternatives`() { - assertEquals( - HONEYPOT_FORMS, - json.decodeFromString(HONEYPOT_JSON), - ) - } - - @Test - fun `deserialize form with container`() { - assertEquals( - CONTAINER_FORMS, - json.decodeFromString(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(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"] - } - } - ] - } - } -} -""" diff --git a/network/src/test/kotlin/com/bitwarden/network/model/FillAssistManifestJsonTest.kt b/network/src/test/kotlin/com/bitwarden/network/model/FillAssistManifestJsonTest.kt deleted file mode 100644 index 4aac50c46a..0000000000 --- a/network/src/test/kotlin/com/bitwarden/network/model/FillAssistManifestJsonTest.kt +++ /dev/null @@ -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(MANIFEST_JSON), - ) - } - - @Test - fun `deserialize manifest with multiple version entries`() { - assertEquals( - MULTI_VERSION_MANIFEST, - json.decodeFromString(MANIFEST_MULTI_VERSION_JSON), - ) - } - - @Test - fun `deserialize manifest with unknown top-level fields is graceful`() { - assertEquals( - FULL_MANIFEST, - json.decodeFromString(MANIFEST_EXTRA_FIELDS_JSON), - ) - } - - @Test - fun `deserialize minimal manifest with null fields`() { - val result = json.decodeFromString("{}") - assertNull(result.buildId) - assertNull(result.maps) - } - - @Test - fun `deserialize manifest with deprecated version entry`() { - assertEquals( - DEPRECATED_MANIFEST, - json.decodeFromString(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" - } - } - } -} -""" diff --git a/network/src/test/kotlin/com/bitwarden/network/service/FillAssistServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/FillAssistServiceTest.kt index 80e625716d..5126c3a588 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/FillAssistServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/FillAssistServiceTest.kt @@ -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, ), ), ),