Add validation to the version sent to the API in x-pm-appversion header

This commit is contained in:
Jorge Martin
2022-03-25 07:58:33 +00:00
committed by MargeBot
parent 75d3ce14f6
commit 316f8a7136
21 changed files with 303 additions and 30 deletions
+28 -5
View File
@@ -6,7 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## Fixed
### Changes
- Added validation for Client Version header (x-pm-appversion).
### New Injection
- ApiManagerFactory now needs an extra `ClientVersionValidator` dependency, which is already included in the Network dagger module.
```kotlin
@Provides
@Singleton
fun provideApiFactory(
...,
clientVersionValidator: ClientVersionValidator,
apiConnectionListener: ApiConnectionListener? = null,
): ApiManagerFactory {
return ApiManagerFactory(
...,
clientVersionValidator = clientVersionValidator,
apiConnectionListener = apiConnectionListener,
)
}
```
### Fixed
- Links, outlined buttons and borderless buttons use text-accent color.
@@ -21,7 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [7.1.5]
## Fixed
### Fixed
- Removed Label dependency from Contact modules.
- Fix invalid strings resources for Plans.
@@ -29,13 +52,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [7.1.4]
## New
### New
- `ProtonSettings` basic composables to build settings screens.
- Enable clients to pass additional filtering function for the plans, according to their needs.
- Added Room LabelConverters. Please add `LabelConverters` to your `TypeConverters`.
## Fixed
### Fixed
- Stop decoding the base64 when decrypting the HashKey (to keep compatibility with drive web & iOS).
- Encode the random bytes in base64 when generating the hash key.
@@ -44,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [7.1.3]
## New
### New
- Added functions to encrypt with compression to the crypto module and to the KeyHolder's context:
`encryptAndSignTextWithCompression` and `encryptAndSignDataWithCompression`
@@ -38,7 +38,7 @@ class CoreExampleApiClient @Inject constructor() : ApiClient {
* Client's value for 'x-pm-appversion' header.
*/
override val appVersionHeader: String
get() = "Android_1.14.0"
get() = "android-mail@$VERSION_NAME"
/**
* Client's value for 'User-Agent' header.
@@ -45,6 +45,7 @@ import me.proton.core.network.domain.ApiClient
import me.proton.core.network.domain.NetworkManager
import me.proton.core.network.domain.NetworkPrefs
import me.proton.core.network.domain.client.ClientIdProvider
import me.proton.core.network.domain.client.ClientVersionValidator
import me.proton.core.network.domain.client.ExtraHeaderProvider
import me.proton.core.network.domain.humanverification.HumanVerificationListener
import me.proton.core.network.domain.humanverification.HumanVerificationProvider
@@ -112,7 +113,8 @@ class NetworkModule {
humanVerificationListener: HumanVerificationListener,
missingScopeListener: MissingScopeListener,
extraHeaderProvider: ExtraHeaderProvider,
apiConnectionListener: ApiConnectionListener? = null
clientVersionValidator: ClientVersionValidator,
apiConnectionListener: ApiConnectionListener? = null,
): ApiManagerFactory {
val certificatePins = if (BuildConfig.USE_DEFAULT_PINS) {
NetWorkDataConstants.DEFAULT_SPKI_PINS
@@ -147,7 +149,8 @@ class NetworkModule {
)
},
extraHeaderProvider = extraHeaderProvider,
apiConnectionListener = apiConnectionListener
apiConnectionListener = apiConnectionListener,
clientVersionValidator = clientVersionValidator,
)
}
+8
View File
@@ -9,6 +9,14 @@ public final class me/proton/core/network/dagger/BuildConfig {
public fun <init> ()V
}
public final class me/proton/core/network/dagger/NetworkModule_ProvideClientVersionValidatorFactory : dagger/internal/Factory {
public fun <init> (Lme/proton/core/network/dagger/NetworkModule;)V
public static fun create (Lme/proton/core/network/dagger/NetworkModule;)Lme/proton/core/network/dagger/NetworkModule_ProvideClientVersionValidatorFactory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/network/domain/client/ClientVersionValidator;
public static fun provideClientVersionValidator (Lme/proton/core/network/dagger/NetworkModule;)Lme/proton/core/network/domain/client/ClientVersionValidator;
}
public final class me/proton/core/network/dagger/NetworkModule_ProvideCookieJarFactory : dagger/internal/Factory {
public fun <init> (Lme/proton/core/network/dagger/NetworkModule;Ljavax/inject/Provider;)V
public static fun create (Lme/proton/core/network/dagger/NetworkModule;Ljavax/inject/Provider;)Lme/proton/core/network/dagger/NetworkModule_ProvideCookieJarFactory;
+3
View File
@@ -24,6 +24,9 @@ plugins {
publishOption.shouldBePublishedAsLib = true
dependencies {
implementation(
project(Module.networkDomain)
)
api(
project(Module.networkData)
)
@@ -24,9 +24,11 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.proton.core.network.data.ProtonCookieStore
import me.proton.core.network.data.client.ClientVersionValidatorImpl
import me.proton.core.network.data.cookie.DiskCookieStorage
import me.proton.core.network.data.cookie.MemoryCookieStorage
import me.proton.core.network.data.ProtonCookieStore
import me.proton.core.network.domain.client.ClientVersionValidator
import javax.inject.Singleton
@Module
@@ -38,4 +40,7 @@ internal class NetworkModule {
persistentStorage = DiskCookieStorage(context, ProtonCookieStore.DISK_COOKIE_STORAGE_NAME),
sessionStorage = MemoryCookieStorage()
)
@Provides
fun provideClientVersionValidator(): ClientVersionValidator = ClientVersionValidatorImpl()
}
+7 -2
View File
@@ -1,6 +1,6 @@
public final class me/proton/core/network/data/ApiManagerFactory {
public fun <init> (Ljava/lang/String;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/serverconnection/ApiConnectionListener;)V
public synthetic fun <init> (Ljava/lang/String;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/serverconnection/ApiConnectionListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/serverconnection/ApiConnectionListener;Lme/proton/core/network/domain/client/ClientVersionValidator;)V
public synthetic fun <init> (Ljava/lang/String;Lme/proton/core/network/domain/ApiClient;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/network/domain/server/ServerTimeListener;Lme/proton/core/network/domain/NetworkManager;Lme/proton/core/network/domain/NetworkPrefs;Lme/proton/core/network/domain/session/SessionProvider;Lme/proton/core/network/domain/session/SessionListener;Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/network/domain/scopes/MissingScopeListener;Lme/proton/core/network/data/ProtonCookieStore;Lkotlinx/coroutines/CoroutineScope;[Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lme/proton/core/network/domain/client/ExtraHeaderProvider;Lme/proton/core/network/domain/serverconnection/ApiConnectionListener;Lme/proton/core/network/domain/client/ClientVersionValidator;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun create (Lme/proton/core/network/domain/session/SessionId;Lkotlin/reflect/KClass;Ljava/util/List;[Ljava/lang/String;Ljava/util/List;)Lme/proton/core/network/domain/ApiManager;
public static synthetic fun create$default (Lme/proton/core/network/data/ApiManagerFactory;Lme/proton/core/network/domain/session/SessionId;Lkotlin/reflect/KClass;Ljava/util/List;[Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lme/proton/core/network/domain/ApiManager;
public final fun getBaseOkHttpClient ()Lokhttp3/OkHttpClient;
@@ -81,6 +81,11 @@ public final class me/proton/core/network/data/client/ClientIdProviderImpl : me/
public fun getClientId (Lme/proton/core/network/domain/session/SessionId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class me/proton/core/network/data/client/ClientVersionValidatorImpl : me/proton/core/network/domain/client/ClientVersionValidator {
public fun <init> ()V
public fun validate (Ljava/lang/String;)Z
}
public final class me/proton/core/network/data/client/ExtraHeaderProviderImpl : me/proton/core/network/domain/client/ExtraHeaderProvider {
public fun <init> ()V
public fun addHeaders ([Lkotlin/Pair;)V
@@ -36,6 +36,7 @@ import me.proton.core.network.domain.DohProvider
import me.proton.core.network.domain.NetworkManager
import me.proton.core.network.domain.NetworkPrefs
import me.proton.core.network.domain.client.ClientIdProvider
import me.proton.core.network.domain.client.ClientVersionValidator
import me.proton.core.network.domain.client.ExtraHeaderProvider
import me.proton.core.network.domain.handlers.ApiConnectionHandler
import me.proton.core.network.domain.handlers.HumanVerificationInvalidHandler
@@ -84,7 +85,8 @@ class ApiManagerFactory(
private val alternativeApiPins: List<String> = Constants.ALTERNATIVE_API_SPKI_PINS,
private val cache: () -> Cache? = { null },
private val extraHeaderProvider: ExtraHeaderProvider? = null,
private val apiConnectionListener: ApiConnectionListener?
private val apiConnectionListener: ApiConnectionListener?,
private val clientVersionValidator: ClientVersionValidator,
) {
@OptIn(ObsoleteCoroutinesApi::class)
@@ -103,6 +105,9 @@ class ApiManagerFactory(
require(apiClient.timeoutSeconds >= ApiClient.MIN_TIMEOUT_SECONDS) {
"Minimum timeout for ApiClient is ${ApiClient.MIN_TIMEOUT_SECONDS} seconds."
}
require(clientVersionValidator.validate(apiClient.appVersionHeader)) {
"Invalid app version code: ${apiClient.appVersionHeader}."
}
OkHttpClient.Builder()
.cache(cache())
.connectTimeout(apiClient.timeoutSeconds, TimeUnit.SECONDS)
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.network.data.client
import me.proton.core.network.domain.client.ClientVersionValidator
class ClientVersionValidatorImpl : ClientVersionValidator {
override fun validate(versionName: String?): Boolean {
val components = versionName?.split("@").orEmpty()
if (components.count() != 2) return false
val (name, version) = components
return isValidName(name) && isValidVersion(version)
}
private fun isValidName(name: String) =
Regex("^[a-z_]+-[a-z_]+(?:-[a-z_]+)?\$").matches(name)
private fun isValidVersion(version: String) =
Regex("^\\d+?\\.\\d+?\\.\\d+?(-((stable|RC|beta|alpha)(\\.\\d+)?|dev)|)?(\\+[0-9A-Za-z\\-]+)?\$")
.matches(version)
}
@@ -48,6 +48,7 @@ import me.proton.core.network.domain.NetworkStatus
import me.proton.core.network.domain.ResponseCodes
import me.proton.core.network.domain.client.ClientId
import me.proton.core.network.domain.client.ClientIdProvider
import me.proton.core.network.domain.client.ClientVersionValidator
import me.proton.core.network.domain.handlers.RefreshTokenHandler
import me.proton.core.network.domain.humanverification.HumanVerificationListener
import me.proton.core.network.domain.humanverification.HumanVerificationProvider
@@ -105,6 +106,9 @@ internal class ApiManagerTests {
@MockK
private lateinit var dohService: DohService
@MockK
private lateinit var clientVersionValidator: ClientVersionValidator
private var time = 0L
private var wallTime = 0L
@@ -144,7 +148,8 @@ internal class ApiManagerTests {
mockk(),
scope,
cache = { null },
apiConnectionListener = null
apiConnectionListener = null,
clientVersionValidator = clientVersionValidator,
)
coEvery { dohService.getAlternativeBaseUrls(any()) } returns listOf(proxy1url)
@@ -24,6 +24,7 @@ import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import me.proton.core.network.data.doh.DnsOverHttpsProviderRFC8484
import me.proton.core.network.data.util.MockApiClient
import me.proton.core.network.data.util.takeRequestWithDefaultTimeout
import me.proton.core.network.domain.NetworkManager
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
@@ -93,6 +94,7 @@ internal class DohProviderTests {
val result = dohProvider.getAlternativeBaseUrls("https://$domain/")!!
assertEquals(listOf("https://proxy.com/"), result)
assertEquals("application/dns-message", webServer.takeRequest().headers["Accept"])
val acceptHeader = webServer.takeRequestWithDefaultTimeout()?.headers?.get("Accept")
assertEquals("application/dns-message", acceptHeader)
}
}
@@ -37,12 +37,14 @@ import me.proton.core.network.data.util.MockSessionListener
import me.proton.core.network.data.util.TestRetrofitApi
import me.proton.core.network.data.util.TestTLSHelper
import me.proton.core.network.data.util.prepareResponse
import me.proton.core.network.data.util.takeRequestWithDefaultTimeout
import me.proton.core.network.domain.ApiManager
import me.proton.core.network.domain.ApiResult
import me.proton.core.network.domain.NetworkManager
import me.proton.core.network.domain.NetworkPrefs
import me.proton.core.network.domain.client.ClientId
import me.proton.core.network.domain.client.ClientIdProvider
import me.proton.core.network.domain.client.ClientVersionValidator
import me.proton.core.network.domain.client.CookieSessionId
import me.proton.core.network.domain.humanverification.HumanVerificationDetails
import me.proton.core.network.domain.humanverification.HumanVerificationListener
@@ -126,6 +128,9 @@ internal class HumanVerificationTests {
private val humanVerificationProvider = mockk<HumanVerificationProvider>()
private val humanVerificationListener = mockk<HumanVerificationListener>()
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
private val clientVersionValidator = mockk<ClientVersionValidator> {
every { validate(any()) } returns true
}
private var sessionListener: SessionListener = MockSessionListener(
onTokenRefreshed = { session -> this.session = session }
@@ -169,7 +174,8 @@ internal class HumanVerificationTests {
cookieJar,
scope,
cache = { null },
apiConnectionListener = null
apiConnectionListener = null,
clientVersionValidator = clientVersionValidator,
)
every { networkManager.isConnectedToNetwork() } returns isNetworkAvailable
@@ -200,7 +206,7 @@ internal class HumanVerificationTests {
pinningInit,
::javaWallClockMs,
prefs,
cookieJar
cookieJar,
)
@After
@@ -321,15 +327,15 @@ internal class HumanVerificationTests {
coEvery { humanVerificationProvider.getHumanVerificationDetails(clientId) } returns humanVerificationDetails
backend(ApiManager.Call(0) { test() })
val headers = webServer.takeRequest().headers
val headers = webServer.takeRequestWithDefaultTimeout()?.headers
verify(exactly = 1) {
humanVerificationDetails.tokenCode
}
verify(exactly = 1) {
humanVerificationDetails.tokenType
}
assertTrue(headers.contains(Pair("x-pm-human-verification-token-type", "captcha")))
assertTrue(headers.contains(Pair("x-pm-human-verification-token", "captcha token")))
assertTrue(headers?.contains(Pair("x-pm-human-verification-token-type", "captcha")) ?: false)
assertTrue(headers?.contains(Pair("x-pm-human-verification-token", "captcha token")) ?: false)
}
@Test
@@ -360,15 +366,15 @@ internal class HumanVerificationTests {
coEvery { humanVerificationProvider.getHumanVerificationDetails(clientId) } returns humanVerificationDetails
backend(ApiManager.Call(0) { test() })
val headers = webServer.takeRequest().headers
val headers = webServer.takeRequestWithDefaultTimeout()?.headers
verify(exactly = 1) {
humanVerificationDetails.tokenCode
}
verify(exactly = 1) {
humanVerificationDetails.tokenType
}
assertTrue(headers.contains(Pair("x-pm-human-verification-token-type", "captcha")))
assertTrue(headers.contains(Pair("x-pm-human-verification-token", "captcha token")))
assertTrue(headers?.contains(Pair("x-pm-human-verification-token-type", "captcha")) ?: false)
assertTrue(headers?.contains(Pair("x-pm-human-verification-token", "captcha token")) ?: false)
}
private fun javaWallClockMs(): Long = System.currentTimeMillis()
@@ -35,12 +35,14 @@ import me.proton.core.network.data.util.MockSessionListener
import me.proton.core.network.data.util.TestRetrofitApi
import me.proton.core.network.data.util.TestTLSHelper
import me.proton.core.network.data.util.prepareResponse
import me.proton.core.network.data.util.takeRequestWithDefaultTimeout
import me.proton.core.network.domain.ApiManager
import me.proton.core.network.domain.ApiResult
import me.proton.core.network.domain.NetworkManager
import me.proton.core.network.domain.NetworkPrefs
import me.proton.core.network.domain.client.ClientId
import me.proton.core.network.domain.client.ClientIdProvider
import me.proton.core.network.domain.client.ClientVersionValidator
import me.proton.core.network.domain.client.ExtraHeaderProvider
import me.proton.core.network.domain.humanverification.HumanVerificationDetails
import me.proton.core.network.domain.humanverification.HumanVerificationListener
@@ -91,6 +93,9 @@ internal class ProtonApiBackendTests {
private val humanVerificationProvider = mockk<HumanVerificationProvider>()
private val humanVerificationListener = mockk<HumanVerificationListener>()
private val missingScopeListener = mockk<MissingScopeListener>(relaxed = true)
private val clientVersionValidator = mockk<ClientVersionValidator> {
every { validate(any()) } returns true
}
private var sessionListener: SessionListener = MockSessionListener(
onTokenRefreshed = { session -> this.session = session }
@@ -136,7 +141,8 @@ internal class ProtonApiBackendTests {
cookieJar,
scope,
cache = { null },
apiConnectionListener = null
apiConnectionListener = null,
clientVersionValidator = clientVersionValidator,
)
every { networkManager.isConnectedToNetwork() } returns isNetworkAvailable
@@ -249,7 +255,7 @@ internal class ProtonApiBackendTests {
webServer.prepareResponse(HttpURLConnection.HTTP_OK, "plain")
val result = backend(ApiManager.Call(0) { testPlain() })
assertEquals("text/plain", webServer.takeRequest().headers["Accept"])
assertEquals("text/plain", webServer.takeRequestWithDefaultTimeout()?.headers?.get("Accept"))
assertTrue(result is ApiResult.Success)
assertEquals("plain", result.value)
@@ -384,8 +390,8 @@ internal class ProtonApiBackendTests {
backend(ApiManager.Call(0) { test() })
val request = webServer.takeRequest()
val headerFound = request.headers.any { it == extraHeader }
val request = webServer.takeRequestWithDefaultTimeout()
val headerFound = request?.headers?.any { it == extraHeader } ?: false
Assert.assertTrue(headerFound)
}
}
@@ -0,0 +1,131 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.network.data.client
import org.junit.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ClientVersionValidatorTests {
private val validator = ClientVersionValidatorImpl()
@Test
fun `Null version will fail validation`() {
val version: String? = null
assertFalse { validator.validate(version) }
}
@Test
fun `Empty version will fail validation`() {
val version = ""
assertFalse { validator.validate(version) }
}
@Test
fun `Only version number info will fail validation`() {
val version = "1.2.3"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with invalid characters in will fail validation`() {
val version = "ANDROID-Mail@1.2.3" // Should be lowercase
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with invalid separators will fail validation`() {
val version = "android_mail_1.2.3"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with valid section info will be validated`() {
val version = "android-mail-somesection_info@1.2.3"
assertTrue { validator.validate(version) }
}
@Test
fun `Version code with no section info will be validated`() {
val version = "android-mail@1.2.3"
assertTrue { validator.validate(version) }
}
@Test
fun `Version code with invalid characters in section info will fail validation`() {
val version = "android-mail-somesection.info@1.2.3"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with empty section info but separator will fail validation`() {
val version = "android-mail-@1.2.3"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with empty version metadata info but separator will fail validation`() {
val version = "android-mail@1.2.3+"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with empty version identifier info but separator will fail validation`() {
val version = "android-mail@1.2.3-"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with invalid characters in version metadata will fail validation`() {
val version = "android-mail@1.2.3+some.metadata"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with versioned dev identifier version will fail validation`() {
val version = "android-mail@1.2.3-dev.1"
assertFalse { validator.validate(version) }
}
@Test
fun `Version code with only dev identifier version will pass validation`() {
val version = "android-mail@1.2.3-dev"
assertTrue { validator.validate(version) }
}
@Test
fun `Version code with versioned stable, RC, beta or alpha identifiers will pass validation`() {
val versionWithStableVersion = "android-mail@1.2.3-stable.1"
val versionWithAlphaVersion = "android-mail@1.2.3-alpha.1"
val versionWithRCVersion = "android-mail@1.2.3-RC.1"
val versionWithBetaVersion = "android-mail@1.2.3-beta.1"
assertTrue { validator.validate(versionWithStableVersion) }
assertTrue { validator.validate(versionWithAlphaVersion) }
assertTrue { validator.validate(versionWithRCVersion) }
assertTrue { validator.validate(versionWithBetaVersion) }
}
@Test
fun `Version code with both version identifier and metadata will pass validation`() {
val version = "android-mail@1.2.3-stable.1+some-metadata"
assertTrue { validator.validate(version) }
}
}
@@ -66,7 +66,7 @@ class MockApiClient : ApiClient {
var forceUpdated = false
override var shouldUseDoh = true
override val appVersionHeader = "TestApp_1.0"
override val appVersionHeader = "android-mail@1.2.3"
override val userAgent = "Test/1.0 (Android 10; brand model)"
override val enableDebugLogging: Boolean = true
@@ -24,6 +24,7 @@ import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import java.io.File
import java.security.KeyStore
import java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
@@ -37,6 +38,8 @@ fun MockWebServer.prepareResponse(code: Int, body: String = "") {
enqueue(response)
}
fun MockWebServer.takeRequestWithDefaultTimeout() = takeRequest(10, TimeUnit.MILLISECONDS)
class TestTLSHelper {
var trustManagers: Array<TrustManager>
+4
View File
@@ -329,6 +329,10 @@ public final class me/proton/core/network/domain/client/ClientIdType$Companion {
public final fun getMap ()Ljava/util/Map;
}
public abstract interface class me/proton/core/network/domain/client/ClientVersionValidator {
public abstract fun validate (Ljava/lang/String;)Z
}
public final class me/proton/core/network/domain/client/CookieSessionId {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton AG and ProtonCore.
*
* ProtonCore is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonCore is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.network.domain.client
interface ClientVersionValidator {
fun validate(versionName: String?): Boolean
}
@@ -30,7 +30,9 @@ import me.proton.core.report.domain.entity.BugReportMeta
import me.proton.core.report.domain.repository.ReportRepository
import javax.inject.Inject
public class ReportRepositoryImpl @Inject constructor(private val apiProvider: ApiProvider) : ReportRepository {
public class ReportRepositoryImpl @Inject constructor(
private val apiProvider: ApiProvider,
) : ReportRepository {
override suspend fun sendReport(
bugReport: BugReport,
meta: BugReportMeta,
@@ -53,7 +53,7 @@ internal class ReportRepositoryImplTest {
email = "email@test"
)
private val testBugReportMeta = BugReportMeta(
appVersionName = "1.2.3",
appVersionName = "android-mail@1.2.3",
clientName = "TestApp",
osName = "Android",
osVersion = "12",
@@ -30,7 +30,7 @@ internal val testBugReport = BugReport(
email = "test@email"
)
internal val testBugReportMeta = BugReportMeta(
appVersionName = "1.2.3",
appVersionName = "android-mail@1.2.3",
clientName = "TestApp",
osName = "Android",
osVersion = "100",