diff --git a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt
index 4391381db..90fd0ec4e 100644
--- a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt
+++ b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt
@@ -167,7 +167,7 @@ internal class SignupViewModel @Inject constructor(
}.catch { error ->
emit(State.Error.Message(error.message))
}.onEach {
- _state.tryEmit(it)
+ _state.emit(it)
}.launchIn(viewModelScope)
fun register(caller: ActivityResultCaller) {
diff --git a/coreexample/build.gradle.kts b/coreexample/build.gradle.kts
index 2b79c413a..64a6437cd 100644
--- a/coreexample/build.gradle.kts
+++ b/coreexample/build.gradle.kts
@@ -141,6 +141,21 @@ fun setupFlavors(testedExtension: TestedExtension) {
buildConfigField("String", buildConfigFieldKeys.HV3_HOST, hv3Host.toBuildConfigValue())
buildConfigField("String", buildConfigFieldKeys.QUARK_HOST, quarkHost.toBuildConfigValue())
}
+ productFlavors.register("mock") {
+ buildConfigField("String", buildConfigFieldKeys.API_HOST, "api.mock".toBuildConfigValue())
+ buildConfigField("String", buildConfigFieldKeys.HV3_HOST, "verify.mock".toBuildConfigValue())
+ buildConfigField("String", buildConfigFieldKeys.QUARK_HOST, "quark.mock".toBuildConfigValue())
+ buildConfigField("Boolean", buildConfigFieldKeys.USE_DEFAULT_PINS, false.toBuildConfigValue())
+
+ dimension = flavorDimensions.env
+ testInstrumentationRunner = "me.proton.core.test.android.ProtonTestRunner"
+ testInstrumentationRunnerArguments["clearPackageData"] = "true"
+
+ testOptions {
+ animationsDisabled = true
+ execution = "ANDROIDX_TEST_ORCHESTRATOR"
+ }
+ }
}
}
@@ -234,12 +249,22 @@ dependencies {
`hilt-android-compiler`
)
+ kaptAndroidTest(
+ `hilt-android-compiler`
+ )
+
androidTestImplementation(
project(Module.androidInstrumentedTest),
+ `android-test-runner`,
`hilt-android-testing`,
- `kotlin-test-junit`
+ `kotlin-test-junit`,
+ `mockk-android`,
+ mockWebServer,
+ uiautomator
)
+ androidTestUtil(`androidx-test-orchestrator`)
+
// Lint - off temporary
// lintChecks(project(Module.lint))
}
diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/ProtonTestRunner.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/ProtonTestRunner.kt
new file mode 100644
index 000000000..c25e554db
--- /dev/null
+++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/ProtonTestRunner.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class ProtonTestRunner : AndroidJUnitRunner() {
+ override fun newApplication(cl: ClassLoader, className: String, context: Context): Application =
+ super.newApplication(cl, HiltTestApplication::class.java.name, context)
+}
diff --git a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/auth/signup/SelectPlanTests.kt b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/auth/signup/SelectPlanTests.kt
index 02228af34..adc5e040b 100644
--- a/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/auth/signup/SelectPlanTests.kt
+++ b/coreexample/src/androidTest/kotlin/me/proton/core/test/android/uitests/tests/medium/auth/signup/SelectPlanTests.kt
@@ -158,7 +158,10 @@ class SelectPlanTests : BaseTest() {
selectPlanRobot?.let {
it.toggleExpandPlan(Dev)
.selectPlan(Dev)
- .verify { googleIAPElementsDisplayed() }
+ .verify {
+ googleIAPElementsDisplayed()
+ nextPaymentProviderButtonDisplayed()
+ }
GoogleIAPRobot()
.close()
diff --git a/coreexample/src/androidTestMock/assets/GET/addresses-no-keys.json b/coreexample/src/androidTestMock/assets/GET/addresses-no-keys.json
new file mode 100644
index 000000000..8b1b6d6bf
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/addresses-no-keys.json
@@ -0,0 +1,22 @@
+{
+ "Code": 1000,
+ "Addresses": [
+ {
+ "ID": "Aj_ugxEsw8G-5twSvHmaaC-R75FBj7szznD7ib4DbBa7cuJxrIITuXMuzUHLMcQQrBglx0jacVwzDq-wGKMphQ==",
+ "DomainID": "l8vWAXHBQmv0u7OVtPbcqMa4iwQaBqowINSQjPrxAr-Da8fVPKUkUcqAq30_BCxj1X0nW70HQRmAa-rIvzmKUA==",
+ "Email": "test-mock-936@proton.black",
+ "Status": 1,
+ "Type": 1,
+ "Receive": 0,
+ "Send": 0,
+ "DisplayName": "",
+ "Signature": "",
+ "Order": 1,
+ "Priority": 1,
+ "ConfirmationState": 1,
+ "HasKeys": 0,
+ "SignedKeyList": null,
+ "Keys": []
+ }
+ ]
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/addresses-with-keys.json b/coreexample/src/androidTestMock/assets/GET/addresses-with-keys.json
new file mode 100644
index 000000000..e321cc0b3
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/addresses-with-keys.json
@@ -0,0 +1,46 @@
+{
+ "Code": 1000,
+ "Addresses": [
+ {
+ "ID": "Aj_ugxEsw8G-5twSvHmaaC-R75FBj7szznD7ib4DbBa7cuJxrIITuXMuzUHLMcQQrBglx0jacVwzDq-wGKMphQ==",
+ "DomainID": "l8vWAXHBQmv0u7OVtPbcqMa4iwQaBqowINSQjPrxAr-Da8fVPKUkUcqAq30_BCxj1X0nW70HQRmAa-rIvzmKUA==",
+ "Email": "test-mock-936@proton.black",
+ "Status": 1,
+ "Type": 1,
+ "Receive": 1,
+ "Send": 1,
+ "DisplayName": "",
+ "Signature": "",
+ "Order": 1,
+ "Priority": 1,
+ "ConfirmationState": 1,
+ "HasKeys": 1,
+ "SignedKeyList": {
+ "MinEpochID": null,
+ "MaxEpochID": null,
+ "ExpectedMinEpochID": 413,
+ "Data": "[{\"Fingerprint\": \"0060f7739d803c34162963bfad3c05fca6a522f1\",\"SHA256Fingerprints\": [\"317add2a2a52233559e42c9a9b0f1cc84dc4532132b415a98b02663d3f9591b6\",\"855d512b2230abbbd058da2a5e210f35c161a2cd5d514b2062265463eee1192c\"],\"Flags\": 3,\"Primary\": 1}]",
+ "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\n\nwnUEABYKACcFAmNIQecJkK08BfympSLxFiEEAGD3c52APDQWKWO/rTwF/Kal\nIvEAAK3ZAQCPrqF9RPcFj/gkfM5IUxN0HUULnXWCJaMhxEinVbTu2gD+OOZI\n4yvWC3fcDpSVYhClNjV6fn6rVLzetG0MbHwmVgs=\n=yPix\n-----END PGP SIGNATURE-----\n"
+ },
+ "Keys": [
+ {
+ "ID": "gG7SLPctbQeU4GiCDYrwPdtGBOwjTo501TUvu-xanerLahsW7uShd29QbBmy8FHXomp20WmH6bWcyKZAu5aMoQ==",
+ "Primary": 1,
+ "Flags": 3,
+ "Fingerprint": "0060f7739d803c34162963bfad3c05fca6a522f1",
+ "Fingerprints": [
+ "0060f7739d803c34162963bfad3c05fca6a522f1",
+ "9381fee3c76b61154cfaeeca2f48c5ae1156905f"
+ ],
+ "PublicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: ProtonMail\n\nxjMEY0bwZxYJKwYBBAHaRw8BAQdA32HWq/s9pn80cTYQzntfCUObXhQdKTYL\ncWhzh5mKVCPNN3Rlc3QtbW9jay05MzZAcHJvdG9uLmJsYWNrIDx0ZXN0LW1v\nY2stOTM2QHByb3Rvbi5ibGFjaz7CjAQTFggAPgUCY0bwZwmQrTwF/KalIvEW\nIQQAYPdznYA8NBYpY7+tPAX8pqUi8QIbAwIeAQIZAQMLCQcCFQgDFgACAiIB\nAAAZlwEA162m1fpQ1Lc4BOIud94Na4PYNLOZG+1XyBn+cZaRFeYA/AmpAYsd\naTK94OOAziF3xzTmogaSOaLlAdK4amyqMUkFwqgEEBYIAFoFAmNIQecJkNTS\nGgVRp7LsFiEENhVDvw2lbaYMs9qU1NIaBVGnsuwsHFRlc3QgT3BlblBHUCBD\nQSA8dGVzdC1vcGVucGdwLWNhQHByb3Rvbi5tZT4FgwDtTgAAACbtAQDLWkDg\nWyIHnmcvKydwf3g1qGa+h8DfnqiqPHDPumtxpgD+OisPcOZocVFvTCyrumXn\nCuydKSCMmEF+dbQp6L5XfgXOOARjRvBnEgorBgEEAZdVAQUBAQdAPgaJsuED\neY1ElpnYaZHraXVHH4CiMYCOXe+BxTl88gcDAQoJwngEGBYIACoFAmNG8GcJ\nkK08BfympSLxFiEEAGD3c52APDQWKWO/rTwF/KalIvECGwwAADVyAQCG85q1\nI4fqiEpB1S+qMykiI5X0vUrxyLvf4UajjduwAQD/auK6+JyYruPq/UV+L9yw\nVxCwZV211YbS1oaQOTElhAI=\n=6Gbr\n-----END PGP PUBLIC KEY BLOCK-----\n",
+ "Active": 1,
+ "Version": 3,
+ "Activation": null,
+ "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\n\nxYYEY0bwZxYJKwYBBAHaRw8BAQdA32HWq/s9pn80cTYQzntfCUObXhQdKTYL\ncWhzh5mKVCP+CQMIUpXLJHRxAopggKwtUzebKlK5y8EYCwho/je4Q9t9drFo\nf++RYhV1Bf/5oVO27Mw9rhVmbONCYSInx5tuJjQ+DhvmqkfqcYruP3kLMGHU\nZ803dGVzdC1tb2NrLTkzNkBwcm90b24uYmxhY2sgPHRlc3QtbW9jay05MzZA\ncHJvdG9uLmJsYWNrPsKMBBMWCAA+BQJjRvBnCZCtPAX8pqUi8RYhBABg93Od\ngDw0Filjv608BfympSLxAhsDAh4BAhkBAwsJBwIVCAMWAAICIgEAABmXAQDX\nrabV+lDUtzgE4i533g1rg9g0s5kb7VfIGf5xlpEV5gD8CakBix1pMr3g44DO\nIXfHNOaiBpI5ouUB0rhqbKoxSQXHiwRjRvBnEgorBgEEAZdVAQUBAQdAPgaJ\nsuEDeY1ElpnYaZHraXVHH4CiMYCOXe+BxTl88gcDAQoJ/gkDCN7T8/EcLqWx\nYJSp14FcyrKeISS5eF/7BtQKrPG8znPt2IJP38dqbplEgzgfS1ypbrKJHf+n\n6gKR/aEVL7TaNqQ/TS3V/Kyt9F3pc7XZ8lzCeAQYFggAKgUCY0bwZwmQrTwF\n/KalIvEWIQQAYPdznYA8NBYpY7+tPAX8pqUi8QIbDAAANXIBAIbzmrUjh+qI\nSkHVL6ozKSIjlfS9SvHIu9/hRqON27ABAP9q4rr4nJiu4+r9RX4v3LBXELBl\nXbXVhtLWhpA5MSWEAg==\n=egGh\n-----END PGP PRIVATE KEY BLOCK-----\n",
+ "Token": null,
+ "Signature": null
+ }
+ ]
+ }
+ ]
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/auth/modulus.json b/coreexample/src/androidTestMock/assets/GET/auth/modulus.json
new file mode 100644
index 000000000..d2a6451a9
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/auth/modulus.json
@@ -0,0 +1,5 @@
+{
+ "Code": 1000,
+ "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n+6itHql7q+rDyAIm+PNYRNpzdqza5KSTQQ5KZCi1apxTgC4jzMvm46Ph7Ipb8L0LnwawV6rfDpG6/xqPqW2Wa0PMUK/AOA7mc4ElkKAyf/bMnk00foJ/jT2TDTvAceXOcUM93deJimL6Kd3T+hqcjZAYTOh4qGBdH8vLKfFgs1Xrcgo0nBZAMcxUkcbZFnWMsCGwHlPbdT8fn8Wzv1h6xyCeT5rEK7L1VIN01Zjv1gKIbHcd2uGiY63NKdYg17rBelZ50taNFZoZk9PbGv6z0xSUGVxZ3pveNyG4oaeFYbyc17bbTSfwYsi4V/8kML6kPcnQZQ/vXsAtbzRVboWqzQ==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAADSJwEA5ee0/SNdABNcsKh3rCp6\n9l6lbFYukxV0i2owa5CCJ6cBANMluXRgjg0LJCYVPnBjhrWB6PCnRMgdCyF9\nHMnOiTUI\n=LOHL\n-----END PGP SIGNATURE-----\n",
+ "ModulusID": "e_DzaCA0dI-vawVq4ab00WFHBMCbZ4ZbVEFBIAxEIkg_ozPFoN4QAQxTjO7vlQ5P9Y-MpUTgUAZ_CICYQz2uxA=="
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/auth/scopes.json b/coreexample/src/androidTestMock/assets/GET/auth/scopes.json
new file mode 100644
index 000000000..4fc2e384c
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/auth/scopes.json
@@ -0,0 +1,17 @@
+{
+ "Code": 1000,
+ "Scope": "4297913728",
+ "Scopes": [
+ "full",
+ "locked",
+ "self",
+ "payments",
+ "keys",
+ "parent",
+ "user",
+ "loggedin",
+ "nondelinquent",
+ "mail",
+ "verified"
+ ]
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/core/v4/events/CfPWFKv7hHURG0mJ5raWYYCcLXKvfCbFoaezy-06aqcxxvbkWdKTxkZUIcanEf7Tw4USrg2vZ_HUIi06YjILLQ==.json b/coreexample/src/androidTestMock/assets/GET/core/v4/events/CfPWFKv7hHURG0mJ5raWYYCcLXKvfCbFoaezy-06aqcxxvbkWdKTxkZUIcanEf7Tw4USrg2vZ_HUIi06YjILLQ==.json
new file mode 100644
index 000000000..654a572a7
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/core/v4/events/CfPWFKv7hHURG0mJ5raWYYCcLXKvfCbFoaezy-06aqcxxvbkWdKTxkZUIcanEf7Tw4USrg2vZ_HUIi06YjILLQ==.json
@@ -0,0 +1,8 @@
+{
+ "Code": 1000,
+ "EventID": "CfPWFKv7hHURG0mJ5raWYYCcLXKvfCbFoaezy-06aqcxxvbkWdKTxkZUIcanEf7Tw4USrg2vZ_HUIi06YjILLQ==",
+ "Refresh": 0,
+ "More": 0,
+ "Pushes": [],
+ "Notices": []
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/core/v4/events/latest.json b/coreexample/src/androidTestMock/assets/GET/core/v4/events/latest.json
new file mode 100644
index 000000000..d9afbd220
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/core/v4/events/latest.json
@@ -0,0 +1,4 @@
+{
+ "Code": 1000,
+ "EventID": "CfPWFKv7hHURG0mJ5raWYYCcLXKvfCbFoaezy-06aqcxxvbkWdKTxkZUIcanEf7Tw4USrg2vZ_HUIi06YjILLQ=="
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/core/v4/features.json b/coreexample/src/androidTestMock/assets/GET/core/v4/features.json
new file mode 100644
index 000000000..b9686994e
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/core/v4/features.json
@@ -0,0 +1,22 @@
+{
+ "Code": 1000,
+ "Features": [
+ {
+ "Code": "ThreadingAndroid",
+ "Type": "boolean",
+ "Global": true,
+ "DefaultValue": true,
+ "Value": true,
+ "Writable": false
+ },
+ {
+ "Code": "CanUserSendFeedback",
+ "Type": "boolean",
+ "Global": true,
+ "DefaultValue": false,
+ "Value": false,
+ "Writable": false
+ }
+ ],
+ "Total": 2
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/domains/available.json b/coreexample/src/androidTestMock/assets/GET/domains/available.json
new file mode 100644
index 000000000..0c9b943f1
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/domains/available.json
@@ -0,0 +1,7 @@
+{
+ "Code": 1000,
+ "Domains": [
+ "proton.black",
+ "protonmail.com.proton.black"
+ ]
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/keys.json b/coreexample/src/androidTestMock/assets/GET/keys.json
new file mode 100644
index 000000000..746031a15
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/keys.json
@@ -0,0 +1,19 @@
+{
+ "Code": 1000,
+ "RecipientType": 1,
+ "MIMEType": "text/html",
+ "Keys": [
+ {
+ "Flags": 3,
+ "PublicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: ProtonMail\n\nxjMEY0fm4hYJKwYBBAHaRw8BAQdAcQcU17vasED/GNSVBFJL8pOsRTLv9ix6\nPVnQan43bVjNR3Rlc3QtZm4yaTM0aEBwYXltZW50cy5wcm90b24uYmxhY2sg\nPHRlc3QtZm4yaTM0aEBwYXltZW50cy5wcm90b24uYmxhY2s+wowEExYIAD4F\nAmNH5uIJkGXQGno3YG6/FiEEm2+6uaYNlaK0+/shZdAaejdgbr8CGwMCHgEC\nGQEDCwkHAhUIAxYAAgIiAQAAT4UA/1FkNRAJ9ErCsx0OiN4JeEZsDuBd0DbF\ni48ejOMocAlWAQCjtN7ylHanFip80t9Bbsd0iw2CJhKwfOddWXY02k9GD8Ko\nBBAWCABaBQJjSThiCZDU0hoFUaey7BYhBDYVQ78NpW2mDLPalNTSGgVRp7Ls\nLBxUZXN0IE9wZW5QR1AgQ0EgPHRlc3Qtb3BlbnBncC1jYUBwcm90b24ubWU+\nBYMA7U4AAACuBgD7BWTrCQToGDVRZKUckkAAgpCgRY6fnv8ZK2JxlFWQ8bAA\n/3347rLgOgtARjj9t1znCjAeoCQbJQw74ge/nSRxgekAzjgEY0fm4hIKKwYB\nBAGXVQEFAQEHQNU+tdqd9yjxaO+gW4XCWwFoCA8SqKa2N6aRZJ6l6zBaAwEK\nCcJ4BBgWCAAqBQJjR+biCZBl0Bp6N2BuvxYhBJtvurmmDZWitPv7IWXQGno3\nYG6/AhsMAADHIQEAqxfglAyZmITQxybwxvipBXQaPcvxWUOxH6h3PxLBu5IB\nANo+nxEII8hQkzdgN3plBNhS3kuD4fwUTTWv1L+Y/8gO\n=/TXu\n-----END PGP PUBLIC KEY BLOCK-----\n"
+ }
+ ],
+ "SignedKeyList": {
+ "MinEpochID": null,
+ "MaxEpochID": null,
+ "ExpectedMinEpochID": 1,
+ "Data": "[{\"Fingerprint\": \"9b6fbab9a60d95a2b4fbfb2165d01a7a37606ebf\",\"SHA256Fingerprints\": [\"aef688d21c4cfa367ff5754862498501f400f87d9ce207b88fa52ccac556c582\",\"731f8442280cf2312ea0bf9ea6fd7542fcb983ff51c1e4c97fa0371c78e7ec41\"],\"Flags\": 3,\"Primary\": 1}]",
+ "Signature": "-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\n\nwnUEABYKACcFAmNJOGIJkGXQGno3YG6/FiEEm2+6uaYNlaK0+/shZdAaejdg\nbr8AAA9PAQCLUDIEFDrqv+lppiWkukJkGsp+tJ8iwSozU7Q1/63UBgD/SfqD\n1R+fea5FwlqdfRXTzfwj6Xafoqe2Rg4tfQRTeAo=\n=vtu/\n-----END PGP SIGNATURE-----\n"
+ },
+ "Warnings": []
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/keys/salts.json b/coreexample/src/androidTestMock/assets/GET/keys/salts.json
new file mode 100644
index 000000000..2d8a76728
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/keys/salts.json
@@ -0,0 +1,13 @@
+{
+ "Code": 1000,
+ "KeySalts": [
+ {
+ "ID": "ktXfDhLWmzv_urlJa4J_hjGWNLOYRwwJbY8N7vJ8iUJjsW6tET3rGLWLU64DhPXcjiXkLS-P-Cex_cnsfiB-2g==",
+ "KeySalt": "KyXt2jZxVpS4Uh3AVwY2HA=="
+ },
+ {
+ "ID": "gG7SLPctbQeU4GiCDYrwPdtGBOwjTo501TUvu-xanerLahsW7uShd29QbBmy8FHXomp20WmH6bWcyKZAu5aMoQ==",
+ "KeySalt": "KyXt2jZxVpS4Uh3AVwY2HA=="
+ }
+ ]
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/mail/v4/settings.json b/coreexample/src/androidTestMock/assets/GET/mail/v4/settings.json
new file mode 100644
index 000000000..e9d5c0517
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/mail/v4/settings.json
@@ -0,0 +1,93 @@
+{
+ "Code": 1000,
+ "MailSettings": {
+ "LastLoginTime": 1665742943,
+ "AutoSaveContacts": 1,
+ "AutoWildcardSearch": 1,
+ "ComposerMode": 0,
+ "FontSize": null,
+ "FontFace": null,
+ "MessageButtons": 0,
+ "ShowImages": 2,
+ "ShowMoved": 0,
+ "ViewMode": 0,
+ "ViewLayout": 0,
+ "SwipeLeft": 3,
+ "SwipeRight": 0,
+ "AlsoArchive": 0,
+ "Hotkeys": 0,
+ "Shortcuts": 1,
+ "PMSignature": 2,
+ "ImageProxy": 2,
+ "TLS": 0,
+ "RightToLeft": 0,
+ "AttachPublicKey": 0,
+ "Sign": 0,
+ "PGPScheme": 16,
+ "PromptPin": 0,
+ "KT": 0,
+ "Autocrypt": 0,
+ "StickyLabels": 0,
+ "ExpandFolders": 0,
+ "ConfirmLink": 1,
+ "DelaySendSeconds": 10,
+ "ThemeType": 0,
+ "ThemeVersion": null,
+ "Theme": "",
+ "DisplayName": "",
+ "Signature": "",
+ "AutoResponder": {
+ "StartTime": 0,
+ "EndTime": 0,
+ "DaysSelected": [],
+ "Repeat": 0,
+ "Subject": "Auto",
+ "Message": "",
+ "IsEnabled": false,
+ "Zone": "Europe/Zurich"
+ },
+ "EnableFolderColor": 0,
+ "InheritParentFolderColor": 1,
+ "SpamAction": null,
+ "BlockSenderConfirmation": null,
+ "MobileSettings": {
+ "MessageToolbar": {
+ "IsCustom": false,
+ "Actions": [
+ "toggle_read",
+ "trash",
+ "move",
+ "label"
+ ]
+ },
+ "ConversationToolbar": {
+ "IsCustom": false,
+ "Actions": [
+ "toggle_read",
+ "trash",
+ "move",
+ "label"
+ ]
+ },
+ "ListToolbar": {
+ "IsCustom": false,
+ "Actions": [
+ "toggle_read",
+ "trash",
+ "move",
+ "label"
+ ]
+ }
+ },
+ "HideEmbeddedImages": 0,
+ "HideRemoteImages": 0,
+ "HideSenderImages": 0,
+ "NumMessagePerPage": 50,
+ "RecipientLimit": 100,
+ "DraftMIMEType": "text/html",
+ "ReceiveMIMEType": "text/html",
+ "ShowMIMEType": "text/html",
+ "PMSignatureReferralLink": 0,
+ "SubmissionAccess": 0
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/organizations-none.json b/coreexample/src/androidTestMock/assets/GET/organizations-none.json
new file mode 100644
index 000000000..5dd88a501
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/organizations-none.json
@@ -0,0 +1,5 @@
+{
+ "Code": 2501,
+ "Error": "You are not a member of an organization",
+ "Details": {}
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/methods.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/methods.json
new file mode 100644
index 000000000..b8fa36be9
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/methods.json
@@ -0,0 +1,4 @@
+{
+ "Code": 1000,
+ "PaymentMethods": []
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/plans.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/plans.json
new file mode 100644
index 000000000..f3655a68a
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/plans.json
@@ -0,0 +1,122 @@
+{
+ "Code": 1000,
+ "Plans": [
+ {
+ "ID": "Wb4NAqmiuqoA7kCHE28y92bBFfN8jaYQCLxHRAB96yGj-bh9SxguXC48_WSU-fRUjdAr-lx95c6rFLplgXyXYA==",
+ "ParentMetaPlanID": "hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==",
+ "Type": 1,
+ "Name": "mail2022",
+ "Title": "Mail Plus",
+ "MaxDomains": 1,
+ "MaxAddresses": 10,
+ "MaxCalendars": 20,
+ "MaxSpace": 16106127360,
+ "MaxMembers": 1,
+ "MaxVPN": 0,
+ "MaxTier": 0,
+ "Services": 1,
+ "Features": 1,
+ "State": 1,
+ "Pricing": {
+ "1": 499,
+ "12": 4788,
+ "24": 8376
+ },
+ "PeriodEnd": {
+ "1": 1668096824,
+ "12": 1696954424,
+ "24": 1728576824
+ },
+ "Currency": "CHF",
+ "Quantity": 1,
+ "Cycle": 1,
+ "Amount": 499,
+ "Vendors": {
+ "Google": {
+ "Plans": {
+ "12": "googlemail_mail2022_12_renewing"
+ },
+ "CustomerID": "cus_google_3KGnMew0qrAfmwXgP0FL"
+ }
+ }
+ },
+ {
+ "ID": "vl-JevUsz3GJc18CC1VOs-qDKqoIWlLiUePdrzFc72-BtxBPHBDZM7ayn8CNQ59Sk4XjDbwwBVpdYrPIFtOvIw==",
+ "ParentMetaPlanID": "hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==",
+ "Type": 1,
+ "Name": "bundle2022",
+ "Title": "Proton Unlimited",
+ "MaxDomains": 3,
+ "MaxAddresses": 15,
+ "MaxCalendars": 20,
+ "MaxSpace": 536870912000,
+ "MaxMembers": 1,
+ "MaxVPN": 10,
+ "MaxTier": 2,
+ "Services": 7,
+ "Features": 1,
+ "State": 1,
+ "Pricing": {
+ "1": 1199,
+ "12": 11988,
+ "24": 19176
+ },
+ "PeriodEnd": {
+ "1": 1668096824,
+ "12": 1696954424,
+ "24": 1728576824
+ },
+ "Currency": "CHF",
+ "Quantity": 1,
+ "Cycle": 1,
+ "Amount": 1199,
+ "Vendors": {
+ "Google": {
+ "Plans": {
+ "12": "googlemail_bundle2022_12_renewing"
+ },
+ "CustomerID": "cus_google_fw4epbT6TRXc0hGyDXKa"
+ }
+ }
+ },
+ {
+ "ID": "l8vWAXHBQmv0u7OVtPbcqMa4iwQaBqowINSQjPrxAr-Da8fVPKUkUcqAq30_BCxj1X0nW70HQRmAa-rIvzmKUA==",
+ "ParentMetaPlanID": "hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==",
+ "Type": 1,
+ "Name": "plus",
+ "Title": "ProtonMail Plus",
+ "MaxDomains": 1,
+ "MaxAddresses": 5,
+ "MaxCalendars": 20,
+ "MaxSpace": 5368709120,
+ "MaxMembers": 1,
+ "MaxVPN": 0,
+ "MaxTier": 0,
+ "Services": 1,
+ "Features": 0,
+ "State": 0,
+ "Pricing": {
+ "1": 500,
+ "12": 4800,
+ "24": 7900
+ },
+ "PeriodEnd": {
+ "1": 1668096824,
+ "12": 1696954424,
+ "24": 1728576824
+ },
+ "Currency": "CHF",
+ "Quantity": 1,
+ "Cycle": 1,
+ "Amount": 500,
+ "Vendors": {
+ "Google": {
+ "Plans": {
+ "12": "googlemail_plus_12_renewing"
+ },
+ "CustomerID": "cus_google_EywMAJN-vpYitcT00qQ_"
+ }
+ }
+ }
+ ]
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/plans/default.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/plans/default.json
new file mode 100644
index 000000000..06282a091
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/plans/default.json
@@ -0,0 +1,21 @@
+{
+ "Code": 1000,
+ "Plans": {
+ "Type": 1,
+ "Name": "free",
+ "Title": "Proton Free",
+ "MaxDomains": 0,
+ "MaxAddresses": 1,
+ "MaxCalendars": 1,
+ "MaxSpace": 524288000,
+ "MaxRewardSpace": 1073741824,
+ "MaxMembers": 1,
+ "MaxVPN": 1,
+ "Services": null,
+ "Features": 0,
+ "Cycle": null,
+ "Currency": null,
+ "Amount": 0,
+ "Quantity": 1
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google-all-disabled.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google-all-disabled.json
new file mode 100644
index 000000000..b9756d6ff
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google-all-disabled.json
@@ -0,0 +1,7 @@
+{
+ "Code": 1000,
+ "Card": 0,
+ "Paypal": 0,
+ "Bitcoin": 0,
+ "InApp": 0
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google-iap-only.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google-iap-only.json
new file mode 100644
index 000000000..889e221a8
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google-iap-only.json
@@ -0,0 +1,7 @@
+{
+ "Code": 1000,
+ "Card": 0,
+ "Paypal": 0,
+ "Bitcoin": 0,
+ "InApp": 1
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google.json
new file mode 100644
index 000000000..f423c56a1
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/status/google.json
@@ -0,0 +1,7 @@
+{
+ "Code": 1000,
+ "Card": 1,
+ "Paypal": 1,
+ "Bitcoin": 0,
+ "InApp": 1
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription-mail-plus-google-managed.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription-mail-plus-google-managed.json
new file mode 100644
index 000000000..da8b7cc58
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription-mail-plus-google-managed.json
@@ -0,0 +1,43 @@
+{
+ "Code": 1000,
+ "Subscription": {
+ "ID": "mfFmlFpj9JFOwsHXeN-p0bjnOppF34tgrr_mUYXJa1UCvXgssVMvkSe3gPo3vgd_zicARNzWsta3Tl8d7xdvqQ==",
+ "InvoiceID": "Je7hvu3ZQBjsED0PcTYXVbIEj8lLhFzgll3I6UNnKBpajE8uIubLADCjb7fJcaXZYL-5kYq23Gckin5Mnn_czA==",
+ "Cycle": 12,
+ "PeriodStart": 1665418938,
+ "PeriodEnd": 1696954938,
+ "CreateTime": 1665418938,
+ "CouponCode": null,
+ "Currency": "CHF",
+ "Amount": 4788,
+ "Discount": 0,
+ "RenewDiscount": 0,
+ "RenewAmount": 4788,
+ "Plans": [
+ {
+ "ID": "Wb4NAqmiuqoA7kCHE28y92bBFfN8jaYQCLxHRAB96yGj-bh9SxguXC48_WSU-fRUjdAr-lx95c6rFLplgXyXYA==",
+ "ParentMetaPlanID": "hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==",
+ "Type": 1,
+ "Name": "mail2022",
+ "Title": "Mail Plus",
+ "MaxDomains": 1,
+ "MaxAddresses": 10,
+ "MaxCalendars": 20,
+ "MaxSpace": 16106127360,
+ "MaxMembers": 1,
+ "MaxVPN": 0,
+ "MaxTier": 0,
+ "Services": 1,
+ "Features": 1,
+ "State": 1,
+ "Cycle": 12,
+ "Currency": "CHF",
+ "Amount": 4788,
+ "Quantity": 1
+ }
+ ],
+ "Renew": 1,
+ "External": 2,
+ "CustomerID": "cus_google_QTqe7W-dkfX09qtIy7Mb"
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription-none.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription-none.json
new file mode 100644
index 000000000..e8e812b14
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription-none.json
@@ -0,0 +1,5 @@
+{
+ "Code": 22110,
+ "Error": "You do not have an active subscription",
+ "Details": {}
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription/check-mail-plus.json b/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription/check-mail-plus.json
new file mode 100644
index 000000000..ae89561d4
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/payments/v4/subscription/check-mail-plus.json
@@ -0,0 +1,13 @@
+{
+ "Code": 1000,
+ "AmountDue": 4788,
+ "Amount": 4788,
+ "Cycle": 12,
+ "CouponDiscount": 0,
+ "Gift": 0,
+ "Currency": "CHF",
+ "UnusedCredit": 0,
+ "Credit": 0,
+ "Proration": 0,
+ "Coupon": null
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/users-no-keys-not-subscribed.json b/coreexample/src/androidTestMock/assets/GET/users-no-keys-not-subscribed.json
new file mode 100644
index 000000000..97774376f
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/users-no-keys-not-subscribed.json
@@ -0,0 +1,24 @@
+{
+ "Code": 1000,
+ "User": {
+ "ID": "VhC0TBoR_SHd7-rjOLtY5lYQ_Dqw9e8Lu54FqyW05ssJ3eEo6hglLat6Aq9M5MaM66EZ2Nr327QQL5zyF15SvQ==",
+ "Name": "test-mock-936",
+ "Currency": "CHF",
+ "Credit": 0,
+ "Type": 1,
+ "CreateTime": 1665679842,
+ "MaxSpace": 524288000,
+ "MaxUpload": 26214400,
+ "UsedSpace": 0,
+ "Subscribed": 0,
+ "Services": 1,
+ "MnemonicStatus": 0,
+ "Role": 0,
+ "Private": 1,
+ "Delinquent": 0,
+ "Keys": [],
+ "ToMigrate": 1,
+ "Email": "test-mock-936@proton.black",
+ "DisplayName": ""
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/users-no-keys-subscribed.json b/coreexample/src/androidTestMock/assets/GET/users-no-keys-subscribed.json
new file mode 100644
index 000000000..4349a50bc
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/users-no-keys-subscribed.json
@@ -0,0 +1,24 @@
+{
+ "Code": 1000,
+ "User": {
+ "ID": "VhC0TBoR_SHd7-rjOLtY5lYQ_Dqw9e8Lu54FqyW05ssJ3eEo6hglLat6Aq9M5MaM66EZ2Nr327QQL5zyF15SvQ==",
+ "Name": "test-mock-936",
+ "Currency": "CHF",
+ "Credit": 0,
+ "Type": 1,
+ "CreateTime": 1665679842,
+ "MaxSpace": 16106127360,
+ "MaxUpload": 26214400,
+ "UsedSpace": 0,
+ "Subscribed": 1,
+ "Services": 1,
+ "MnemonicStatus": 0,
+ "Role": 2,
+ "Private": 1,
+ "Delinquent": 0,
+ "Keys": [],
+ "ToMigrate": 1,
+ "Email": "test-mock-936@proton.black",
+ "DisplayName": ""
+ }
+}
\ No newline at end of file
diff --git a/coreexample/src/androidTestMock/assets/GET/users-with-keys-not-subscribed.json b/coreexample/src/androidTestMock/assets/GET/users-with-keys-not-subscribed.json
new file mode 100644
index 000000000..cc6006cc8
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/users-with-keys-not-subscribed.json
@@ -0,0 +1,35 @@
+{
+ "Code": 1000,
+ "User": {
+ "ID": "VhC0TBoR_SHd7-rjOLtY5lYQ_Dqw9e8Lu54FqyW05ssJ3eEo6hglLat6Aq9M5MaM66EZ2Nr327QQL5zyF15SvQ==",
+ "Name": "test-mock-936",
+ "Currency": "CHF",
+ "Credit": 0,
+ "Type": 1,
+ "CreateTime": 1665679842,
+ "MaxSpace": 524288000,
+ "MaxUpload": 26214400,
+ "UsedSpace": 248329,
+ "Subscribed": 0,
+ "Services": 1,
+ "MnemonicStatus": 0,
+ "Role": 0,
+ "Private": 1,
+ "Delinquent": 0,
+ "Keys": [
+ {
+ "ID": "ktXfDhLWmzv_urlJa4J_hjGWNLOYRwwJbY8N7vJ8iUJjsW6tET3rGLWLU64DhPXcjiXkLS-P-Cex_cnsfiB-2g==",
+ "Version": 3,
+ "Primary": 1,
+ "RecoverySecret": null,
+ "RecoverySecretSignature": null,
+ "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\n\nxYYEY0bwZhYJKwYBBAHaRw8BAQdA1yDTR70NxF/vJVpYKVcA85nGx202WhTF\nr8B7CjTyRaj+CQMIawSpxnhvxmlgWQkR6VxIOn/FvaCnnHoEeZpBB7glqril\nSvo7VaLxVDNmTlK0iXLAdN9IDJzoTstqem3kP0eDsV1VllYogeEZXycMVLdl\ngs03dGVzdC1tb2NrLTkzNkBwcm90b24uYmxhY2sgPHRlc3QtbW9jay05MzZA\ncHJvdG9uLmJsYWNrPsKMBBMWCAA+BQJjRvBmCZCx8Eg4YZqbdxYhBPwQ6P4+\nLphcoA4eGLHwSDhhmpt3AhsDAh4BAhkBAwsJBwIVCAMWAAICIgEAAPGgAQDP\nFL4EJ4KERUaQe+CqRcN9n0eht+an0gB4DI13laL4DAD9GoT/l7rgzE/6S/mZ\nvs+pIXDa2vVe9lFeYPwAH6Q5cg3HiwRjRvBmEgorBgEEAZdVAQUBAQdAWcGA\nsFE9w5OQUIJSpltFD0NF0a7tqqCJzta33ylLfjYDAQoJ/gkDCLzXimofJ0Ov\nYMsejJrofkHZXKaBE1Jl2yxf4jMxBGGB93G/INhsjF0fNX0XJfucUKJ4Lyk+\nMaiQsvD4dMUSJjxuRqNyfOp3NR1SYa0+hSfCeAQYFggAKgUCY0bwZgmQsfBI\nOGGam3cWIQT8EOj+Pi6YXKAOHhix8Eg4YZqbdwIbDAAAKUEA/jgGKBtfNvRp\nqNn3b+1V7pOCNhabyDge22SlCFWI1Q6+AP4xt9jEIRVDzGVEnhxG44o82cyC\nTZLysQbBbsnilK4BBw==\n=pKOS\n-----END PGP PRIVATE KEY BLOCK-----\n",
+ "Fingerprint": "fc10e8fe3e2e985ca00e1e18b1f04838619a9b77",
+ "Active": 1
+ }
+ ],
+ "ToMigrate": 1,
+ "Email": "test-mock-936@proton.black",
+ "DisplayName": ""
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/users-with-keys-subscribed.json b/coreexample/src/androidTestMock/assets/GET/users-with-keys-subscribed.json
new file mode 100644
index 000000000..4fd4e9a2a
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/users-with-keys-subscribed.json
@@ -0,0 +1,35 @@
+{
+ "Code": 1000,
+ "User": {
+ "ID": "VhC0TBoR_SHd7-rjOLtY5lYQ_Dqw9e8Lu54FqyW05ssJ3eEo6hglLat6Aq9M5MaM66EZ2Nr327QQL5zyF15SvQ==",
+ "Name": "test-mock-936",
+ "Currency": "CHF",
+ "Credit": 0,
+ "Type": 1,
+ "CreateTime": 1665679842,
+ "MaxSpace": 524288000,
+ "MaxUpload": 26214400,
+ "UsedSpace": 248329,
+ "Subscribed": 1,
+ "Services": 1,
+ "MnemonicStatus": 0,
+ "Role": 0,
+ "Private": 1,
+ "Delinquent": 0,
+ "Keys": [
+ {
+ "ID": "ktXfDhLWmzv_urlJa4J_hjGWNLOYRwwJbY8N7vJ8iUJjsW6tET3rGLWLU64DhPXcjiXkLS-P-Cex_cnsfiB-2g==",
+ "Version": 3,
+ "Primary": 1,
+ "RecoverySecret": null,
+ "RecoverySecretSignature": null,
+ "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\n\nxYYEY0bwZhYJKwYBBAHaRw8BAQdA1yDTR70NxF/vJVpYKVcA85nGx202WhTF\nr8B7CjTyRaj+CQMIawSpxnhvxmlgWQkR6VxIOn/FvaCnnHoEeZpBB7glqril\nSvo7VaLxVDNmTlK0iXLAdN9IDJzoTstqem3kP0eDsV1VllYogeEZXycMVLdl\ngs03dGVzdC1tb2NrLTkzNkBwcm90b24uYmxhY2sgPHRlc3QtbW9jay05MzZA\ncHJvdG9uLmJsYWNrPsKMBBMWCAA+BQJjRvBmCZCx8Eg4YZqbdxYhBPwQ6P4+\nLphcoA4eGLHwSDhhmpt3AhsDAh4BAhkBAwsJBwIVCAMWAAICIgEAAPGgAQDP\nFL4EJ4KERUaQe+CqRcN9n0eht+an0gB4DI13laL4DAD9GoT/l7rgzE/6S/mZ\nvs+pIXDa2vVe9lFeYPwAH6Q5cg3HiwRjRvBmEgorBgEEAZdVAQUBAQdAWcGA\nsFE9w5OQUIJSpltFD0NF0a7tqqCJzta33ylLfjYDAQoJ/gkDCLzXimofJ0Ov\nYMsejJrofkHZXKaBE1Jl2yxf4jMxBGGB93G/INhsjF0fNX0XJfucUKJ4Lyk+\nMaiQsvD4dMUSJjxuRqNyfOp3NR1SYa0+hSfCeAQYFggAKgUCY0bwZgmQsfBI\nOGGam3cWIQT8EOj+Pi6YXKAOHhix8Eg4YZqbdwIbDAAAKUEA/jgGKBtfNvRp\nqNn3b+1V7pOCNhabyDge22SlCFWI1Q6+AP4xt9jEIRVDzGVEnhxG44o82cyC\nTZLysQbBbsnilK4BBw==\n=pKOS\n-----END PGP PRIVATE KEY BLOCK-----\n",
+ "Fingerprint": "fc10e8fe3e2e985ca00e1e18b1f04838619a9b77",
+ "Active": 1
+ }
+ ],
+ "ToMigrate": 1,
+ "Email": "test-mock-936@proton.black",
+ "DisplayName": ""
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/GET/users/available.json b/coreexample/src/androidTestMock/assets/GET/users/available.json
new file mode 100644
index 000000000..71d65cc8f
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/GET/users/available.json
@@ -0,0 +1,3 @@
+{
+ "Code": 1000
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/auth.json b/coreexample/src/androidTestMock/assets/POST/auth.json
new file mode 100644
index 000000000..f74402fe9
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/auth.json
@@ -0,0 +1,36 @@
+{
+ "Code": 1000,
+ "ExpiresIn": 86400,
+ "LocalID": 0,
+ "TokenType": "Bearer",
+ "AccessToken": "wt3ibg32nfmd2bp7mmwr2m6wg4pgq5vn",
+ "RefreshToken": "3kknjkum6ofwricgli35fewa3vfxudl6",
+ "Scopes": [
+ "full",
+ "self",
+ "payments",
+ "keys",
+ "parent",
+ "user",
+ "loggedin",
+ "nondelinquent",
+ "verified"
+ ],
+ "UID": "b6xkue4uqtq5gsfedxzbvbu5zpicoy2e",
+ "UserID": "VhC0TBoR_SHd7-rjOLtY5lYQ_Dqw9e8Lu54FqyW05ssJ3eEo6hglLat6Aq9M5MaM66EZ2Nr327QQL5zyF15SvQ==",
+ "EventID": "8nz6Put0cKKKinZGaC6mBrLJvn9-dvwFRcfD3LxUS-AnpJLf6GKgohuVkeSyyqqvcnip1p5guikT8DgcoZlVmQ==",
+ "PasswordMode": 1,
+ "ServerProof": "hD3IgA8zIRu7/0uCMns2DTNv0abl++eEaGEbSCKmgknmNL9JGk0MgaUM3ut+t7jTfi3PJKtNZCU5kMN5oKFzMDiAojN8pG6XrjOorbxDN5PmfP7XLUHgFolLlxgb93sKyZPie73J/cf22VHkmlHmFTlbMZs2gqTrUDF/D4xdewlrA5weu4myP/baanaPaLR9UzFrgOMe69k6Ei2iUbOtuntUyj4zqiK1JvuyDjVZSYD91aVwx25VNLZMc+bTAWTwYjVQO+z6J7CnyDyRBu9OXzO54YwpiMdhWKvHZIPx5xxPWJzPtA4SaFl+t57/K1C23MY4OHnxKaQvllN5nKG+rQ==",
+ "Scope": "full self payments keys parent user loggedin nondelinquent verified",
+ "Uid": "b6xkue4uqtq5gsfedxzbvbu5zpicoy2e",
+ "TwoFactor": 0,
+ "2FA": {
+ "Enabled": 0,
+ "FIDO2": {
+ "AuthenticationOptions": null,
+ "RegisteredKeys": []
+ },
+ "TOTP": 0
+ },
+ "TemporaryPassword": 0
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/auth/info.json b/coreexample/src/androidTestMock/assets/POST/auth/info.json
new file mode 100644
index 000000000..8b4965fb3
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/auth/info.json
@@ -0,0 +1,8 @@
+{
+ "Code": 1000,
+ "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n+6itHql7q+rDyAIm+PNYRNpzdqza5KSTQQ5KZCi1apxTgC4jzMvm46Ph7Ipb8L0LnwawV6rfDpG6/xqPqW2Wa0PMUK/AOA7mc4ElkKAyf/bMnk00foJ/jT2TDTvAceXOcUM93deJimL6Kd3T+hqcjZAYTOh4qGBdH8vLKfFgs1Xrcgo0nBZAMcxUkcbZFnWMsCGwHlPbdT8fn8Wzv1h6xyCeT5rEK7L1VIN01Zjv1gKIbHcd2uGiY63NKdYg17rBelZ50taNFZoZk9PbGv6z0xSUGVxZ3pveNyG4oaeFYbyc17bbTSfwYsi4V/8kML6kPcnQZQ/vXsAtbzRVboWqzQ==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAADSJwEA5ee0/SNdABNcsKh3rCp6\n9l6lbFYukxV0i2owa5CCJ6cBANMluXRgjg0LJCYVPnBjhrWB6PCnRMgdCyF9\nHMnOiTUI\n=LOHL\n-----END PGP SIGNATURE-----\n",
+ "ServerEphemeral": "IEvE4XmCjgbOLGcwSaE5vrrDK40Fudsc9rUkJhUqhhAdWRZWnQEI+JhFpzFrMraZX2gcMQaR1xXaYYuSjIlbWLHz0q5lctbDPkiFsBrGsYoRjKuqfbH6KjsNNZwb4rdyyLETT1yNnCCuOlUmF4c6YQ6+fIZJeqwsrrN1Pw/I1xSjxVAMwz42QxSor+C6abp3MpOlB62Tup+VCrPEz2SM+bAj/VFjcSi3pFr7vn/ZYHMTCvBUvBzrm4XbclWAgfEp3Vpm5HMSvzI1IHrKMO01uumMJru7PRVqS7GWEHaeXlxZGwX2/BT68aBhkSYS1JlCk5fWxyIQ81uK4uFE4N8FNA==",
+ "Version": 4,
+ "Salt": "v0qLXBUgfQBIcQ==",
+ "SRPSession": "3c649026f09aa25bedc37a15b471682d"
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/keys/setup.json b/coreexample/src/androidTestMock/assets/POST/keys/setup.json
new file mode 100644
index 000000000..cc6006cc8
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/keys/setup.json
@@ -0,0 +1,35 @@
+{
+ "Code": 1000,
+ "User": {
+ "ID": "VhC0TBoR_SHd7-rjOLtY5lYQ_Dqw9e8Lu54FqyW05ssJ3eEo6hglLat6Aq9M5MaM66EZ2Nr327QQL5zyF15SvQ==",
+ "Name": "test-mock-936",
+ "Currency": "CHF",
+ "Credit": 0,
+ "Type": 1,
+ "CreateTime": 1665679842,
+ "MaxSpace": 524288000,
+ "MaxUpload": 26214400,
+ "UsedSpace": 248329,
+ "Subscribed": 0,
+ "Services": 1,
+ "MnemonicStatus": 0,
+ "Role": 0,
+ "Private": 1,
+ "Delinquent": 0,
+ "Keys": [
+ {
+ "ID": "ktXfDhLWmzv_urlJa4J_hjGWNLOYRwwJbY8N7vJ8iUJjsW6tET3rGLWLU64DhPXcjiXkLS-P-Cex_cnsfiB-2g==",
+ "Version": 3,
+ "Primary": 1,
+ "RecoverySecret": null,
+ "RecoverySecretSignature": null,
+ "PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: ProtonMail\n\nxYYEY0bwZhYJKwYBBAHaRw8BAQdA1yDTR70NxF/vJVpYKVcA85nGx202WhTF\nr8B7CjTyRaj+CQMIawSpxnhvxmlgWQkR6VxIOn/FvaCnnHoEeZpBB7glqril\nSvo7VaLxVDNmTlK0iXLAdN9IDJzoTstqem3kP0eDsV1VllYogeEZXycMVLdl\ngs03dGVzdC1tb2NrLTkzNkBwcm90b24uYmxhY2sgPHRlc3QtbW9jay05MzZA\ncHJvdG9uLmJsYWNrPsKMBBMWCAA+BQJjRvBmCZCx8Eg4YZqbdxYhBPwQ6P4+\nLphcoA4eGLHwSDhhmpt3AhsDAh4BAhkBAwsJBwIVCAMWAAICIgEAAPGgAQDP\nFL4EJ4KERUaQe+CqRcN9n0eht+an0gB4DI13laL4DAD9GoT/l7rgzE/6S/mZ\nvs+pIXDa2vVe9lFeYPwAH6Q5cg3HiwRjRvBmEgorBgEEAZdVAQUBAQdAWcGA\nsFE9w5OQUIJSpltFD0NF0a7tqqCJzta33ylLfjYDAQoJ/gkDCLzXimofJ0Ov\nYMsejJrofkHZXKaBE1Jl2yxf4jMxBGGB93G/INhsjF0fNX0XJfucUKJ4Lyk+\nMaiQsvD4dMUSJjxuRqNyfOp3NR1SYa0+hSfCeAQYFggAKgUCY0bwZgmQsfBI\nOGGam3cWIQT8EOj+Pi6YXKAOHhix8Eg4YZqbdwIbDAAAKUEA/jgGKBtfNvRp\nqNn3b+1V7pOCNhabyDge22SlCFWI1Q6+AP4xt9jEIRVDzGVEnhxG44o82cyC\nTZLysQbBbsnilK4BBw==\n=pKOS\n-----END PGP PRIVATE KEY BLOCK-----\n",
+ "Fingerprint": "fc10e8fe3e2e985ca00e1e18b1f04838619a9b77",
+ "Active": 1
+ }
+ ],
+ "ToMigrate": 1,
+ "Email": "test-mock-936@proton.black",
+ "DisplayName": ""
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription-mail-plus-google-managed.json b/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription-mail-plus-google-managed.json
new file mode 100644
index 000000000..da8b7cc58
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription-mail-plus-google-managed.json
@@ -0,0 +1,43 @@
+{
+ "Code": 1000,
+ "Subscription": {
+ "ID": "mfFmlFpj9JFOwsHXeN-p0bjnOppF34tgrr_mUYXJa1UCvXgssVMvkSe3gPo3vgd_zicARNzWsta3Tl8d7xdvqQ==",
+ "InvoiceID": "Je7hvu3ZQBjsED0PcTYXVbIEj8lLhFzgll3I6UNnKBpajE8uIubLADCjb7fJcaXZYL-5kYq23Gckin5Mnn_czA==",
+ "Cycle": 12,
+ "PeriodStart": 1665418938,
+ "PeriodEnd": 1696954938,
+ "CreateTime": 1665418938,
+ "CouponCode": null,
+ "Currency": "CHF",
+ "Amount": 4788,
+ "Discount": 0,
+ "RenewDiscount": 0,
+ "RenewAmount": 4788,
+ "Plans": [
+ {
+ "ID": "Wb4NAqmiuqoA7kCHE28y92bBFfN8jaYQCLxHRAB96yGj-bh9SxguXC48_WSU-fRUjdAr-lx95c6rFLplgXyXYA==",
+ "ParentMetaPlanID": "hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==",
+ "Type": 1,
+ "Name": "mail2022",
+ "Title": "Mail Plus",
+ "MaxDomains": 1,
+ "MaxAddresses": 10,
+ "MaxCalendars": 20,
+ "MaxSpace": 16106127360,
+ "MaxMembers": 1,
+ "MaxVPN": 0,
+ "MaxTier": 0,
+ "Services": 1,
+ "Features": 1,
+ "State": 1,
+ "Cycle": 12,
+ "Currency": "CHF",
+ "Amount": 4788,
+ "Quantity": 1
+ }
+ ],
+ "Renew": 1,
+ "External": 2,
+ "CustomerID": "cus_google_QTqe7W-dkfX09qtIy7Mb"
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription-mail-plus-proton-managed.json b/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription-mail-plus-proton-managed.json
new file mode 100644
index 000000000..ebf1a5f4b
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription-mail-plus-proton-managed.json
@@ -0,0 +1,42 @@
+{
+ "Code": 1000,
+ "Subscription": {
+ "ID": "zL42loPYyWGerUan59_znvVTLBz71E84dssmJHVsMscJk7ScqcIrBQdQMvGLl9C8w9wiPAh6M4FZV_Jw9WTtDw==",
+ "InvoiceID": "LkFiOm3_dZjYLrp9FwvOr-k5JEW8jB0VJlWd-zfbf4KxuPSxBAm_tqsxxvoq1TNzBaDU9vlwL9i-bOAmv1A3iA==",
+ "Cycle": 12,
+ "PeriodStart": 1665745161,
+ "PeriodEnd": 1697281161,
+ "CreateTime": 1665745164,
+ "CouponCode": null,
+ "Currency": "CHF",
+ "Amount": 4788,
+ "Discount": 0,
+ "RenewDiscount": 0,
+ "RenewAmount": 4788,
+ "Plans": [
+ {
+ "ID": "Wb4NAqmiuqoA7kCHE28y92bBFfN8jaYQCLxHRAB96yGj-bh9SxguXC48_WSU-fRUjdAr-lx95c6rFLplgXyXYA==",
+ "ParentMetaPlanID": "hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==",
+ "Type": 1,
+ "Name": "mail2022",
+ "Title": "Mail Plus",
+ "MaxDomains": 1,
+ "MaxAddresses": 10,
+ "MaxCalendars": 20,
+ "MaxSpace": 16106127360,
+ "MaxMembers": 1,
+ "MaxVPN": 0,
+ "MaxTier": 0,
+ "Services": 1,
+ "Features": 1,
+ "State": 1,
+ "Cycle": 12,
+ "Currency": "CHF",
+ "Amount": 4788,
+ "Quantity": 1
+ }
+ ],
+ "Renew": 1,
+ "External": 0
+ }
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription/check.json b/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription/check.json
new file mode 100644
index 000000000..ae89561d4
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/payments/v4/subscription/check.json
@@ -0,0 +1,13 @@
+{
+ "Code": 1000,
+ "AmountDue": 4788,
+ "Amount": 4788,
+ "Cycle": 12,
+ "CouponDiscount": 0,
+ "Gift": 0,
+ "Currency": "CHF",
+ "UnusedCredit": 0,
+ "Credit": 0,
+ "Proration": 0,
+ "Coupon": null
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/payments/v4/tokens.json b/coreexample/src/androidTestMock/assets/POST/payments/v4/tokens.json
new file mode 100644
index 000000000..dbb1a5a00
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/payments/v4/tokens.json
@@ -0,0 +1,5 @@
+{
+ "Code": 1000,
+ "Token": "21-Uv1NzluEO-4ZPHHjA87zU",
+ "Status": 1
+}
diff --git a/coreexample/src/androidTestMock/assets/POST/v4/users.json b/coreexample/src/androidTestMock/assets/POST/v4/users.json
new file mode 100644
index 000000000..97774376f
--- /dev/null
+++ b/coreexample/src/androidTestMock/assets/POST/v4/users.json
@@ -0,0 +1,24 @@
+{
+ "Code": 1000,
+ "User": {
+ "ID": "VhC0TBoR_SHd7-rjOLtY5lYQ_Dqw9e8Lu54FqyW05ssJ3eEo6hglLat6Aq9M5MaM66EZ2Nr327QQL5zyF15SvQ==",
+ "Name": "test-mock-936",
+ "Currency": "CHF",
+ "Credit": 0,
+ "Type": 1,
+ "CreateTime": 1665679842,
+ "MaxSpace": 524288000,
+ "MaxUpload": 26214400,
+ "UsedSpace": 0,
+ "Subscribed": 0,
+ "Services": 1,
+ "MnemonicStatus": 0,
+ "Role": 0,
+ "Private": 1,
+ "Delinquent": 0,
+ "Keys": [],
+ "ToMigrate": 1,
+ "Email": "test-mock-936@proton.black",
+ "DisplayName": ""
+ }
+}
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestComponent.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestComponent.kt
new file mode 100644
index 000000000..fddeedf2c
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestComponent.kt
@@ -0,0 +1,106 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android
+
+import androidx.work.WorkManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
+import io.mockk.mockk
+import me.proton.android.core.coreexample.di.NetworkConstantsModule
+import me.proton.android.core.coreexample.di.WorkManagerModule
+import me.proton.core.crypto.common.context.CryptoContext
+import me.proton.core.crypto.common.keystore.KeyStoreCrypto
+import me.proton.core.crypto.common.pgp.PGPCrypto
+import me.proton.core.crypto.common.srp.SrpCrypto
+import me.proton.core.crypto.dagger.CoreCryptoModule
+import me.proton.core.network.data.di.AlternativeApiPins
+import me.proton.core.network.data.di.CertificatePins
+import me.proton.core.network.data.di.DohProviderUrls
+import me.proton.core.paymentiap.dagger.CorePaymentIapBillingModule
+import me.proton.core.paymentiap.domain.BillingClientFactory
+import me.proton.core.test.android.mocks.FakeKeyStoreCrypto
+import me.proton.core.test.android.mocks.FakePGPCrypto
+import me.proton.core.test.android.mocks.FakeSrpCrypto
+import me.proton.core.test.android.mocks.FakeBillingClientFactory
+import javax.inject.Singleton
+
+@Module
+@TestInstallIn(
+ components = [SingletonComponent::class],
+ replaces = [
+ CoreCryptoModule::class,
+ CorePaymentIapBillingModule::class,
+ NetworkConstantsModule::class,
+ WorkManagerModule::class
+ ]
+)
+object TestComponent {
+ // region CoreCryptoModule
+ @Provides
+ @Singleton
+ fun provideKeyStoreCrypto(): KeyStoreCrypto = FakeKeyStoreCrypto()
+
+ @Provides
+ @Singleton
+ fun provideCryptoContext(
+ keyStoreCrypto: KeyStoreCrypto,
+ srpCrypto: SrpCrypto
+ ): CryptoContext = object : CryptoContext {
+ override val keyStoreCrypto: KeyStoreCrypto = keyStoreCrypto
+ override val pgpCrypto: PGPCrypto = FakePGPCrypto()
+ override val srpCrypto: SrpCrypto = srpCrypto
+ }
+
+ @Provides
+ @Singleton
+ fun provideSrpCrypto(): SrpCrypto = FakeSrpCrypto()
+ // endregion
+
+ // region CorePaymentIapBillingModule
+ @Provides
+ @Singleton
+ fun provideTestBillingClientFactory(): FakeBillingClientFactory = FakeBillingClientFactory()
+
+ @Provides
+ @Singleton
+ fun provideBillingClientProvider(factory: FakeBillingClientFactory): BillingClientFactory = factory
+ // endregion
+
+ // region NetworkConstantsModule
+ @DohProviderUrls
+ @Provides
+ fun provideDohProviderUrls(): Array = emptyArray()
+
+ @CertificatePins
+ @Provides
+ fun provideCertificatePins() = emptyArray()
+
+ @AlternativeApiPins
+ @Provides
+ fun provideAlternativeApiPins() = emptyList()
+ // endregion
+
+ // region WorkManagerModule
+ @Provides
+ @Singleton
+ fun provideWorkManager(): WorkManager = mockk(relaxed = true)
+ // endregion
+}
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestWebServerDispatcher.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestWebServerDispatcher.kt
new file mode 100644
index 000000000..c8a04244d
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/TestWebServerDispatcher.kt
@@ -0,0 +1,110 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android
+
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.RecordedRequest
+import java.text.SimpleDateFormat
+import java.util.Date
+
+/** Dispatcher for [okhttp3.mockwebserver.MockWebServer].
+ * By default, it will try to find and return the contents of a file,
+ * that correspond to the request path. For example, for request
+ * `GET /auth/modulus`, it will load a file from assets directory: `GET/auth/modulus.json`.
+ * If a file doesn't exist, it'll return 404.
+ * If you don't want the default behavior, you can override the response
+ * by calling [mockFromAssets] or [mock].
+ */
+class TestWebServerDispatcher : Dispatcher() {
+ private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'")
+ private val responses = mutableMapOf>()
+
+ override fun dispatch(request: RecordedRequest): MockResponse {
+ val mockResponses = responses[MockedEndpoint(request.method!!, request.requestUrl?.encodedPath!!)]
+ val queuedResponse = if (mockResponses != null && mockResponses.size > 1) {
+ mockResponses.removeFirstOrNull()
+ } else {
+ mockResponses?.first()
+ }
+ val mockResponse = queuedResponse ?: kotlin.runCatching {
+ val body = loadFromAssets("${request.method}${request.requestUrl?.encodedPath}.json")
+ MockResponse().setResponseCode(200).setBody(body)
+ }.getOrNull()
+
+ val response = mockResponse?.applyDefaultHeaders()
+ Log.d("TestWebServerDispatcher", "Request $request")
+
+ return if (response != null) {
+ Log.d("TestWebServerDispatcher", "Response $response")
+ response
+ } else {
+ Log.e("TestWebServerDispatcher", "Response not found for request $request")
+ MockResponse().setResponseCode(404)
+ }
+ }
+
+ private fun MockResponse.applyDefaultHeaders(): MockResponse =
+ addHeader("Set-Cookie", "Session-Id=test-session-id")
+ .addHeader("Date", dateFormat.format(Date()))
+
+ /** Mocks a response for a request.
+ * You can call this method multiple times, even with the same [requestMethod] and [requestPath].
+ * In that case, the responses will be queued. Once they are served, they are removed,
+ * so the next response for the same request will be served next.
+ * @param requestMethod The HTTP method name ("GET", "POST" etc.)
+ * @param requestPath The path of the incoming request, starting with `/`.
+ * @param responsePath The path of the asset file which will be used as a response body.
+ * @param responseCode The HTTP code for the response.
+ */
+ fun mockFromAssets(
+ requestMethod: String,
+ requestPath: String,
+ responsePath: String,
+ responseCode: Int = 200
+ ) {
+ val body = loadFromAssets(responsePath)
+ val response = MockResponse().setResponseCode(responseCode).setBody(body)
+ mock(
+ requestMethod = requestMethod,
+ requestPath = requestPath,
+ mockResponse = response
+ )
+ }
+
+ fun mock(requestMethod: String, requestPath: String, mockResponse: MockResponse) {
+ val key = MockedEndpoint(method = requestMethod, path = requestPath)
+ val queuedResponses = responses[key] ?: mutableListOf().also {
+ responses[key] = it
+ }
+ queuedResponses.add(mockResponse)
+ }
+
+ private fun loadFromAssets(path: String): String {
+ val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
+ return assetManager.open(path).bufferedReader().use { it.readText() }
+ }
+
+ private data class MockedEndpoint(
+ val method: String,
+ val path: String
+ )
+}
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/BillingClientExt.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/BillingClientExt.kt
new file mode 100644
index 000000000..613ae3265
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/BillingClientExt.kt
@@ -0,0 +1,150 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android.mocks
+
+import com.android.billingclient.api.AcknowledgePurchaseResponseListener
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.ProductDetails.PricingPhase
+import com.android.billingclient.api.ProductDetailsResponseListener
+import com.android.billingclient.api.Purchase
+import com.android.billingclient.api.PurchasesResponseListener
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryPurchasesParams
+import io.mockk.every
+import io.mockk.justRun
+import io.mockk.mockk
+
+internal fun BillingClient.mockBillingClientSuccess(listeners: () -> List) {
+ val purchases = listOf(mockk {
+ every { accountIdentifiers } returns mockk {
+ every { obfuscatedAccountId } returns "obfuscated-acc-id"
+ }
+ every { isAcknowledged } returns true
+ every { orderId } returns "order-id"
+ every { packageName } returns "package-name"
+ every { purchaseTime } returns 0
+ every { products } returns listOf("googlemail_mail2022_12_renewing")
+ every { purchaseState } returns Purchase.PurchaseState.PURCHASED
+ every { purchaseToken } returns "google-purchase-token"
+ })
+
+ mockStartConnection(BillingClient.BillingResponseCode.OK)
+ mockLaunchBillingFlow(
+ BillingClient.BillingResponseCode.OK,
+ listeners,
+ BillingClient.BillingResponseCode.OK,
+ purchases
+ )
+ mockQueryProductDetailsAsync(
+ BillingClient.BillingResponseCode.OK,
+ priceAmountMicros = 99_990_000,
+ priceCurrencyCode = "CHF",
+ formattedPrice = "99.99",
+ googleProductId = "googlemail_mail2022_12_renewing"
+ )
+ mockQueryPurchasesAsync(
+ BillingClient.BillingResponseCode.OK,
+ purchases
+ )
+ mockAcknowledgePurchase(BillingClient.BillingResponseCode.OK)
+}
+
+internal fun BillingClient.mockAcknowledgePurchase(@BillingClient.BillingResponseCode responseCode: Int) {
+ every { acknowledgePurchase(any(), any()) } answers {
+ val listener = args[1] as AcknowledgePurchaseResponseListener
+ listener.onAcknowledgePurchaseResponse(
+ BillingResult.newBuilder().setResponseCode(responseCode).build()
+ )
+ }
+}
+
+internal fun BillingClient.mockStartConnection(@BillingClient.BillingResponseCode responseCode: Int) {
+ every { startConnection(any()) } answers {
+ val listener = args[0] as BillingClientStateListener
+ listener.onBillingSetupFinished(
+ BillingResult.newBuilder().setResponseCode(responseCode).build()
+ )
+ }
+ justRun { endConnection() }
+}
+
+internal fun BillingClient.mockLaunchBillingFlow(
+ @BillingClient.BillingResponseCode responseCode: Int,
+ listeners: () -> List,
+ @BillingClient.BillingResponseCode purchaseResponseCode: Int,
+ purchases: List
+) {
+ every { launchBillingFlow(any(), any()) } answers {
+ listeners().forEach {
+ it.onPurchasesUpdated(
+ BillingResult.newBuilder().setResponseCode(purchaseResponseCode).build(),
+ purchases
+ )
+ }
+ BillingResult.newBuilder().setResponseCode(responseCode).build()
+ }
+}
+
+internal fun BillingClient.mockQueryProductDetailsAsync(
+ @BillingClient.BillingResponseCode responseCode: Int,
+ priceAmountMicros: Long,
+ priceCurrencyCode: String,
+ formattedPrice: String,
+ googleProductId: String
+) {
+ val pricingPhase = mockk {
+ every { this@mockk.priceAmountMicros } returns priceAmountMicros
+ every { this@mockk.priceCurrencyCode } returns priceCurrencyCode
+ every { this@mockk.formattedPrice } returns formattedPrice
+ }
+ val productDetailsList = listOf(mockk(relaxed = true) {
+ every { productId } returns googleProductId
+ every { subscriptionOfferDetails } returns listOf(
+ mockk {
+ every { offerToken } returns "offer-token"
+ every { pricingPhases } returns mockk {
+ every { pricingPhaseList } returns listOf(pricingPhase)
+ }
+ }
+ )
+ })
+ every { queryProductDetailsAsync(any(), any()) } answers {
+ val listener = args[1] as ProductDetailsResponseListener
+ listener.onProductDetailsResponse(
+ BillingResult.newBuilder().setResponseCode(responseCode).build(),
+ productDetailsList
+ )
+ }
+}
+
+internal fun BillingClient.mockQueryPurchasesAsync(
+ @BillingClient.BillingResponseCode responseCode: Int,
+ purchases: List
+) {
+ every { queryPurchasesAsync(any(), any()) } answers {
+ val listener = args[1] as PurchasesResponseListener
+ listener.onQueryPurchasesResponse(
+ BillingResult.newBuilder().setResponseCode(responseCode).build(),
+ purchases
+ )
+ }
+}
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeBillingClientFactory.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeBillingClientFactory.kt
new file mode 100644
index 000000000..91d906cb2
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeBillingClientFactory.kt
@@ -0,0 +1,118 @@
+/*
+ * 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 .
+ */
+
+@file:Suppress("DEPRECATION")
+
+package me.proton.core.test.android.mocks
+
+import android.app.Activity
+import com.android.billingclient.api.AcknowledgePurchaseParams
+import com.android.billingclient.api.AcknowledgePurchaseResponseListener
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingFlowParams
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ConsumeParams
+import com.android.billingclient.api.ConsumeResponseListener
+import com.android.billingclient.api.InAppMessageParams
+import com.android.billingclient.api.InAppMessageResponseListener
+import com.android.billingclient.api.PriceChangeConfirmationListener
+import com.android.billingclient.api.PriceChangeFlowParams
+import com.android.billingclient.api.ProductDetailsResponseListener
+import com.android.billingclient.api.PurchaseHistoryResponseListener
+import com.android.billingclient.api.PurchasesResponseListener
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryProductDetailsParams
+import com.android.billingclient.api.QueryPurchaseHistoryParams
+import com.android.billingclient.api.QueryPurchasesParams
+import com.android.billingclient.api.SkuDetailsParams
+import com.android.billingclient.api.SkuDetailsResponseListener
+import io.mockk.mockk
+import me.proton.core.paymentiap.domain.BillingClientFactory
+
+class FakeBillingClientFactory : BillingClientFactory {
+ private val _listeners = mutableListOf()
+
+ val billingClient: BillingClient = mockk()
+ val listeners: List = _listeners
+
+ override fun invoke(purchasesUpdatedListener: PurchasesUpdatedListener): BillingClient {
+ _listeners.add(purchasesUpdatedListener)
+ return FakeBillingClient(billingClient) { _listeners.remove(purchasesUpdatedListener) }
+ }
+}
+
+@Suppress("OVERRIDE_DEPRECATION")
+private class FakeBillingClient(
+ private val mock: BillingClient,
+ private val onConnectionEnd: () -> Unit
+) : BillingClient() {
+ override fun getConnectionState(): Int = mock.connectionState
+
+ override fun isFeatureSupported(feature: String): BillingResult = mock.isFeatureSupported(feature)
+
+ override fun launchBillingFlow(activity: Activity, params: BillingFlowParams): BillingResult =
+ mock.launchBillingFlow(activity, params)
+
+ override fun showInAppMessages(
+ activity: Activity,
+ params: InAppMessageParams,
+ listener: InAppMessageResponseListener
+ ): BillingResult = mock.showInAppMessages(activity, params, listener)
+
+ override fun acknowledgePurchase(params: AcknowledgePurchaseParams, listener: AcknowledgePurchaseResponseListener) =
+ mock.acknowledgePurchase(params, listener)
+
+ override fun consumeAsync(params: ConsumeParams, listener: ConsumeResponseListener) =
+ mock.consumeAsync(params, listener)
+
+ override fun endConnection() {
+ mock.endConnection()
+ onConnectionEnd()
+ }
+
+ override fun launchPriceChangeConfirmationFlow(
+ activity: Activity,
+ params: PriceChangeFlowParams,
+ listener: PriceChangeConfirmationListener
+ ) = mock.launchPriceChangeConfirmationFlow(activity, params, listener)
+
+ override fun queryProductDetailsAsync(params: QueryProductDetailsParams, listener: ProductDetailsResponseListener) =
+ mock.queryProductDetailsAsync(params, listener)
+
+ override fun queryPurchaseHistoryAsync(
+ params: QueryPurchaseHistoryParams,
+ listener: PurchaseHistoryResponseListener
+ ) = mock.queryPurchaseHistoryAsync(params, listener)
+
+ override fun queryPurchaseHistoryAsync(skuType: String, listener: PurchaseHistoryResponseListener) =
+ mock.queryPurchaseHistoryAsync(skuType, listener)
+
+ override fun queryPurchasesAsync(params: QueryPurchasesParams, listener: PurchasesResponseListener) =
+ mock.queryPurchasesAsync(params, listener)
+
+ override fun queryPurchasesAsync(skuType: String, listener: PurchasesResponseListener) =
+ mock.queryPurchasesAsync(skuType, listener)
+
+ override fun querySkuDetailsAsync(params: SkuDetailsParams, listener: SkuDetailsResponseListener) =
+ mock.querySkuDetailsAsync(params, listener)
+
+ override fun startConnection(listener: BillingClientStateListener) = mock.startConnection(listener)
+
+ override fun isReady(): Boolean = mock.isReady
+}
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeKeyStoreCrypto.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeKeyStoreCrypto.kt
new file mode 100644
index 000000000..da72ce1fb
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeKeyStoreCrypto.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android.mocks
+
+import me.proton.core.crypto.common.keystore.EncryptedByteArray
+import me.proton.core.crypto.common.keystore.EncryptedString
+import me.proton.core.crypto.common.keystore.KeyStoreCrypto
+import me.proton.core.crypto.common.keystore.PlainByteArray
+
+class FakeKeyStoreCrypto : KeyStoreCrypto {
+ override fun isUsingKeyStore(): Boolean = false
+ override fun encrypt(value: String): EncryptedString = value
+ override fun decrypt(value: EncryptedString): String = value
+ override fun encrypt(value: PlainByteArray): EncryptedByteArray = EncryptedByteArray(value.array)
+ override fun decrypt(value: EncryptedByteArray): PlainByteArray = PlainByteArray(value.array)
+}
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakePGPCrypto.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakePGPCrypto.kt
new file mode 100644
index 000000000..4e648baed
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakePGPCrypto.kt
@@ -0,0 +1,367 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android.mocks
+
+import io.mockk.mockk
+import me.proton.core.crypto.common.pgp.Armored
+import me.proton.core.crypto.common.pgp.DataPacket
+import me.proton.core.crypto.common.pgp.DecryptedData
+import me.proton.core.crypto.common.pgp.DecryptedFile
+import me.proton.core.crypto.common.pgp.DecryptedText
+import me.proton.core.crypto.common.pgp.EncryptedFile
+import me.proton.core.crypto.common.pgp.EncryptedMessage
+import me.proton.core.crypto.common.pgp.EncryptedPacket
+import me.proton.core.crypto.common.pgp.EncryptedSignature
+import me.proton.core.crypto.common.pgp.HashKey
+import me.proton.core.crypto.common.pgp.KeyPacket
+import me.proton.core.crypto.common.pgp.PGPCrypto
+import me.proton.core.crypto.common.pgp.PGPHeader
+import me.proton.core.crypto.common.pgp.SessionKey
+import me.proton.core.crypto.common.pgp.Signature
+import me.proton.core.crypto.common.pgp.Unarmored
+import me.proton.core.crypto.common.pgp.UnlockedKey
+import me.proton.core.crypto.common.pgp.VerificationTime
+import java.io.File
+import java.util.Base64
+
+class FakePGPCrypto : PGPCrypto {
+ private val decoder = Base64.getDecoder()
+ private val encoder = Base64.getEncoder()
+
+ override fun isPublicKey(key: Armored): Boolean {
+ return mockk()
+ }
+
+ override fun isPrivateKey(key: Armored): Boolean {
+ return mockk()
+ }
+
+ override fun isValidKey(key: Armored): Boolean {
+ return true
+ }
+
+ override fun lock(unlockedKey: Unarmored, passphrase: ByteArray): Armored {
+ return decoder.decode(unlockedKey).decodeToString()
+ }
+
+ override fun unlock(privateKey: Armored, passphrase: ByteArray): UnlockedKey {
+ return object : UnlockedKey {
+ override val value: Unarmored get() = encoder.encode(privateKey.encodeToByteArray())
+ override fun close() = Unit
+ }
+ }
+
+ override fun encryptText(plainText: String, publicKey: Armored): EncryptedMessage {
+ return plainText
+ }
+
+ override fun encryptData(data: ByteArray, publicKey: Armored): EncryptedMessage {
+ return encoder.encodeToString(data)
+ }
+
+ override fun encryptData(data: ByteArray, sessionKey: SessionKey): DataPacket {
+ return data
+ }
+
+ override fun encryptFile(source: File, destination: File, sessionKey: SessionKey): EncryptedFile {
+ TODO("Not yet implemented: encryptFile")
+ }
+
+ override fun encryptAndSignText(plainText: String, publicKey: Armored, unlockedKey: Unarmored): EncryptedMessage {
+ TODO("Not yet implemented: encryptAndSignText")
+ }
+
+ override fun encryptAndSignTextWithCompression(
+ plainText: String,
+ publicKey: Armored,
+ unlockedKey: Unarmored
+ ): EncryptedMessage {
+ TODO("Not yet implemented: encryptAndSignTextWithCompression")
+ }
+
+ override fun encryptAndSignData(data: ByteArray, publicKey: Armored, unlockedKey: Unarmored): EncryptedMessage {
+ TODO("Not yet implemented: encryptAndSignData")
+ }
+
+ override fun encryptAndSignDataWithCompression(
+ data: ByteArray,
+ publicKey: Armored,
+ unlockedKey: Unarmored
+ ): EncryptedMessage {
+ TODO("Not yet implemented: encryptAndSignDataWithCompression")
+ }
+
+ override fun encryptAndSignData(data: ByteArray, sessionKey: SessionKey, unlockedKey: Unarmored): DataPacket {
+ TODO("Not yet implemented: encryptAndSignData")
+ }
+
+ override fun encryptAndSignFile(
+ source: File,
+ destination: File,
+ sessionKey: SessionKey,
+ unlockedKey: Unarmored
+ ): EncryptedFile {
+ TODO("Not yet implemented: encryptAndSignFile")
+ }
+
+ override fun encryptSessionKey(sessionKey: SessionKey, publicKey: Armored): KeyPacket {
+ TODO("Not yet implemented: encryptSessionKey")
+ }
+
+ override fun encryptSessionKeyWithPassword(sessionKey: SessionKey, password: ByteArray): KeyPacket {
+ TODO("Not yet implemented: encryptSessionKeyWithPassword")
+ }
+
+ override fun decryptText(message: EncryptedMessage, unlockedKey: Unarmored): String {
+ TODO("Not yet implemented: decryptText")
+ }
+
+ override fun decryptData(message: EncryptedMessage, unlockedKey: Unarmored): ByteArray {
+ TODO("Not yet implemented: decryptData")
+ }
+
+ override fun decryptData(data: DataPacket, sessionKey: SessionKey): ByteArray {
+ TODO("Not yet implemented: decryptData")
+ }
+
+ override fun decryptFile(source: EncryptedFile, destination: File, sessionKey: SessionKey): DecryptedFile {
+ TODO("Not yet implemented: decryptFile")
+ }
+
+ override fun decryptAndVerifyText(
+ message: EncryptedMessage,
+ publicKeys: List,
+ unlockedKeys: List,
+ time: VerificationTime
+ ): DecryptedText {
+ TODO("Not yet implemented: decryptAndVerifyText")
+ }
+
+ override fun decryptAndVerifyData(
+ message: EncryptedMessage,
+ publicKeys: List,
+ unlockedKeys: List,
+ time: VerificationTime
+ ): DecryptedData {
+ TODO("Not yet implemented: decryptAndVerifyData")
+ }
+
+ override fun decryptAndVerifyData(
+ data: DataPacket,
+ sessionKey: SessionKey,
+ publicKeys: List,
+ time: VerificationTime
+ ): DecryptedData {
+ TODO("Not yet implemented: decryptAndVerifyData")
+ }
+
+ override fun decryptAndVerifyFile(
+ source: EncryptedFile,
+ destination: File,
+ sessionKey: SessionKey,
+ publicKeys: List,
+ time: VerificationTime
+ ): DecryptedFile {
+ TODO("Not yet implemented: decryptAndVerifyFile")
+ }
+
+ override fun decryptSessionKey(keyPacket: KeyPacket, unlockedKey: Unarmored): SessionKey {
+ TODO("Not yet implemented: decryptSessionKey")
+ }
+
+ override fun decryptSessionKeyWithPassword(keyPacket: KeyPacket, password: ByteArray): SessionKey {
+ TODO("Not yet implemented: decryptSessionKeyWithPassword")
+ }
+
+ override fun signText(plainText: String, unlockedKey: Unarmored): Signature {
+ return "${plainText.hashCode()}"
+ }
+
+ override fun signData(data: ByteArray, unlockedKey: Unarmored): Signature {
+ TODO("Not yet implemented: signData")
+ }
+
+ override fun signFile(file: File, unlockedKey: Unarmored): Signature {
+ TODO("Not yet implemented: signFile")
+ }
+
+ override fun signTextEncrypted(
+ plainText: String,
+ unlockedKey: Unarmored,
+ encryptionKeys: List
+ ): EncryptedSignature {
+ TODO("Not yet implemented: signTextEncrypted")
+ }
+
+ override fun signDataEncrypted(
+ data: ByteArray,
+ unlockedKey: Unarmored,
+ encryptionKeys: List
+ ): EncryptedSignature {
+ TODO("Not yet implemented: signDataEncrypted")
+ }
+
+ override fun signFileEncrypted(
+ file: File,
+ unlockedKey: Unarmored,
+ encryptionKeys: List
+ ): EncryptedSignature {
+ TODO("Not yet implemented: signFileEncrypted")
+ }
+
+ override fun verifyText(
+ plainText: String,
+ signature: Armored,
+ publicKey: Armored,
+ time: VerificationTime
+ ): Boolean {
+ return "${plainText.hashCode()}" == signature
+ }
+
+ override fun verifyData(data: ByteArray, signature: Armored, publicKey: Armored, time: VerificationTime): Boolean {
+ TODO("Not yet implemented: verifyData")
+ }
+
+ override fun verifyFile(
+ file: DecryptedFile,
+ signature: Armored,
+ publicKey: Armored,
+ time: VerificationTime
+ ): Boolean {
+ TODO("Not yet implemented: verifyFile")
+ }
+
+ override fun getVerifiedTimestampOfText(
+ plainText: String,
+ signature: Armored,
+ publicKey: Armored,
+ time: VerificationTime
+ ): Long? {
+ TODO("Not yet implemented: getVerifiedTimestampOfText")
+ }
+
+ override fun getVerifiedTimestampOfData(
+ data: ByteArray,
+ signature: Armored,
+ publicKey: Armored,
+ time: VerificationTime
+ ): Long? {
+ TODO("Not yet implemented: getVerifiedTimestampOfData")
+ }
+
+ override fun verifyTextEncrypted(
+ plainText: String,
+ encryptedSignature: EncryptedSignature,
+ privateKey: Unarmored,
+ publicKeys: List,
+ time: VerificationTime
+ ): Boolean {
+ TODO("Not yet implemented: verifyTextEncrypted")
+ }
+
+ override fun verifyDataEncrypted(
+ data: ByteArray,
+ encryptedSignature: EncryptedSignature,
+ privateKey: Unarmored,
+ publicKeys: List,
+ time: VerificationTime
+ ): Boolean {
+ TODO("Not yet implemented: verifyDataEncrypted")
+ }
+
+ override fun verifyFileEncrypted(
+ file: File,
+ encryptedSignature: EncryptedSignature,
+ privateKey: Unarmored,
+ publicKeys: List,
+ time: VerificationTime
+ ): Boolean {
+ TODO("Not yet implemented: verifyFileEncrypted")
+ }
+
+ override fun getArmored(data: Unarmored, header: PGPHeader): Armored {
+ TODO("Not yet implemented: getArmored")
+ }
+
+ override fun getUnarmored(data: Armored): Unarmored {
+ TODO("Not yet implemented: getUnarmored")
+ }
+
+ override fun getEncryptedPackets(message: EncryptedMessage): List {
+ TODO("Not yet implemented: getEncryptedPackets")
+ }
+
+ override fun getPublicKey(privateKey: Armored): Armored {
+ return privateKey
+ }
+
+ override fun getFingerprint(key: Armored): String {
+ TODO("Not yet implemented: getFingerprint")
+ }
+
+ override fun getJsonSHA256Fingerprints(key: Armored): String {
+ TODO("Not yet implemented: getJsonSHA256Fingerprints")
+ }
+
+ override fun getBase64Encoded(array: ByteArray): String {
+ TODO("Not yet implemented: getBase64Encoded")
+ }
+
+ override fun getBase64Decoded(string: String): ByteArray {
+ TODO("Not yet implemented: getBase64Decoded")
+ }
+
+ override fun getPassphrase(password: ByteArray, encodedSalt: String): ByteArray {
+ return password
+ }
+
+ override fun generateNewSessionKey(): SessionKey {
+ TODO("Not yet implemented: generateNewSessionKey")
+ }
+
+ override fun generateNewHashKey(): HashKey {
+ TODO("Not yet implemented: generateNewHashKey")
+ }
+
+ override fun generateNewKeySalt(): String {
+ TODO("Not yet implemented: generateNewKeySalt")
+ }
+
+ override fun generateNewToken(size: Long): ByteArray {
+ TODO("Not yet implemented: generateNewToken")
+ }
+
+ override fun generateRandomBytes(size: Long): ByteArray {
+ TODO("Not yet implemented: generateRandomBytes")
+ }
+
+ override fun generateNewPrivateKey(username: String, domain: String, passphrase: ByteArray): Armored {
+ TODO("Not yet implemented: generateNewPrivateKey")
+ }
+
+ override fun updatePrivateKeyPassphrase(
+ privateKey: Armored,
+ passphrase: ByteArray,
+ newPassphrase: ByteArray
+ ): Armored {
+ TODO("Not yet implemented: updatePrivateKeyPassphrase")
+ }
+
+ override fun updateTime(epochSeconds: Long) = Unit
+}
\ No newline at end of file
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeSrpCrypto.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeSrpCrypto.kt
new file mode 100644
index 000000000..25c40fa16
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/mocks/FakeSrpCrypto.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android.mocks
+
+import me.proton.core.crypto.common.srp.Auth
+import me.proton.core.crypto.common.srp.SrpCrypto
+import me.proton.core.crypto.common.srp.SrpProofs
+
+@Suppress("MaxLineLength")
+class FakeSrpCrypto : SrpCrypto {
+ override fun generateSrpProofs(
+ username: String,
+ password: ByteArray,
+ version: Long,
+ salt: String,
+ modulus: String,
+ serverEphemeral: String
+ ): SrpProofs {
+ return SrpProofs(
+ clientEphemeral = "6PckVdFlaPSR8UhlCCnLg5Wpmob++1s1EPGOqjhR2etOgqnfQd102t0LDeM8QKO90yPsDbnSjjGcLkK7nWeCLhORNCs71XldJg8QP5ffwN4deXakWw5GOdMBqBMtHLAy9elCewg9B0wxjdYimbdKia7Zk/HyqMKK2Skd3JpRPNNUxK798G4dlISe7UaQ2L3PEIx5aRp+WmlRfT3oq0I4qspjrU64u+44KkchAi+/yQFj4PJQVDsc6NnGL28zGH7OIwmclOQgRZ70+4TiifrnoDfpewg+z37er5t/exYkhckfUaAt4EGQyc2zh8JtV1uu3/GjS8M5bKkzDZ2iHo6snA==",
+ clientProof = "HkY/EcSS75nYf0IPcIfQEIx7Q5+P8t1KQJLTVibFFgeGNB8UNcBpTQO14ih3RWakP/gXrP0fq+ucYU8BluanISMaxXAR6wbPTLEYRQ0vAJJaHVNhOQJ3P31hklh6AKrXikaVU5+z9fToL/TSfb36E4Vkaj0TYenNo4rDKEnK2xuaLk2oezz8g+CjCnd1ekjCRv+zD2dEZPIrcYH6dVwM98YMxov8pzBGEaafMxNNo2GLifWKixAESAnLnLob8dnomv8tZTwZnSkOrT5aFQovQzq+42RwEprTP0fVHt7PnfFkdUzEUFLvbrnWz3QLx7b2zvaTMrW7eQ8AH48IdHyQlQ==",
+ expectedServerProof = "hD3IgA8zIRu7/0uCMns2DTNv0abl++eEaGEbSCKmgknmNL9JGk0MgaUM3ut+t7jTfi3PJKtNZCU5kMN5oKFzMDiAojN8pG6XrjOorbxDN5PmfP7XLUHgFolLlxgb93sKyZPie73J/cf22VHkmlHmFTlbMZs2gqTrUDF/D4xdewlrA5weu4myP/baanaPaLR9UzFrgOMe69k6Ei2iUbOtuntUyj4zqiK1JvuyDjVZSYD91aVwx25VNLZMc+bTAWTwYjVQO+z6J7CnyDyRBu9OXzO54YwpiMdhWKvHZIPx5xxPWJzPtA4SaFl+t57/K1C23MY4OHnxKaQvllN5nKG+rQ=="
+ )
+ }
+
+ override fun calculatePasswordVerifier(
+ username: String,
+ password: ByteArray,
+ modulusId: String,
+ modulus: String
+ ): Auth {
+ return Auth(
+ version = 4,
+ modulusId = "e_DzaCA0dI-vawVq4ab00WFHBMCbZ4ZbVEFBIAxEIkg_ozPFoN4QAQxTjO7vlQ5P9Y-MpUTgUAZ_CICYQz2uxA==",
+ salt = "v0qLXBUgfQBIcQ==",
+ verifier = "jHbPpeayCOGVuLK/cT+g/2Pw9UFgNapy2IDD1GELHej2uCaNfN2uHRoy/XGL27Ngrorb1nkt5ZWFvW/6HKjyUxn14h7ECU6jG6l+tkVLLd8RlDuxQ+PAoSJ4Zuog6H9yEupfuT0x1v6UuITS/i+PRVA4NQ47LaLXpAgiosiX9aLx5g67v8gbMSB50bydgJZakFXAbBiU/mbZwCpJzmyIlxbQSU0CGXfInN0uMbfOJ5AyBkLRZZCYVkWjyzNKABGkBS8nNx3l0p5LtNN5W4F59w27zxHYBli+zPGPgAbNqww4PjSSnhM1GFCF/VbGNnAcMlzwerI2vuzBSCqHAv2/CA=="
+ )
+ }
+}
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/uitests/giap/GiapUpgradeTests.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/uitests/giap/GiapUpgradeTests.kt
new file mode 100644
index 000000000..6a10a2cd0
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/uitests/giap/GiapUpgradeTests.kt
@@ -0,0 +1,152 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android.uitests.giap
+
+import androidx.test.core.app.ActivityScenario
+import com.android.billingclient.api.BillingClient
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import me.proton.android.core.coreexample.CoreExampleLogger
+import me.proton.android.core.coreexample.MainActivity
+import me.proton.core.network.data.di.BaseProtonApiUrl
+import me.proton.core.test.android.TestWebServerDispatcher
+import me.proton.core.test.android.mocks.FakeBillingClientFactory
+import me.proton.core.test.android.mocks.mockBillingClientSuccess
+import me.proton.core.test.android.robots.auth.AddAccountRobot
+import me.proton.core.test.android.robots.payments.GoogleIAPRobot
+import me.proton.core.test.android.uitests.CoreexampleRobot
+import me.proton.core.util.kotlin.CoreLogger
+import okhttp3.HttpUrl
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Rule
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import me.proton.core.test.android.plugins.data.Plan as TestPlan
+
+@HiltAndroidTest
+class GiapUpgradeTests {
+ private val testUsername = "test-mock-936"
+ private val testPassword = "password"
+
+ @get:Rule(order = Rule.DEFAULT_ORDER - 1)
+ val hiltAndroidRule = HiltAndroidRule(this)
+
+ @BindValue
+ @BaseProtonApiUrl
+ lateinit var baseProtonApiUrl: HttpUrl
+
+ @Inject
+ lateinit var billingClientFactory: FakeBillingClientFactory
+
+ private val billingClient: BillingClient get() = billingClientFactory.billingClient
+ private lateinit var dispatcher: TestWebServerDispatcher
+ private lateinit var webServer: MockWebServer
+
+ @BeforeTest
+ fun setUp() {
+ Timber.plant(Timber.DebugTree())
+ CoreLogger.set(CoreExampleLogger())
+
+ dispatcher = TestWebServerDispatcher()
+ webServer = MockWebServer().apply {
+ dispatcher = this@GiapUpgradeTests.dispatcher
+ }
+ baseProtonApiUrl = webServer.url("/")
+
+ hiltAndroidRule.inject()
+ }
+
+ @AfterTest
+ fun tearDown() {
+ webServer.shutdown()
+ }
+
+ @Test
+ fun freeUserUpgradeAllProvidersAvailable() {
+ freeUserCanUpgrade()
+ }
+
+ @Test
+ fun freeUserUpgradeOnlyGiapAvailable() {
+ dispatcher.mockFromAssets(
+ "GET", "/payments/v4/status/google",
+ "GET/payments/v4/status/google-iap-only.json"
+ )
+ freeUserCanUpgrade()
+ }
+
+ private fun freeUserCanUpgrade() {
+ billingClient.mockBillingClientSuccess { billingClientFactory.listeners }
+
+ dispatcher.mockFromAssets(
+ "GET", "/payments/v4/subscription",
+ "GET/payments/v4/subscription-none.json", 422
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/payments/v4/subscription",
+ "GET/payments/v4/subscription-mail-plus-google-managed.json"
+ )
+
+ dispatcher.mockFromAssets(
+ "GET", "/users",
+ "GET/users-with-keys-not-subscribed.json"
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/organizations",
+ "GET/organizations-none.json", 422
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/addresses",
+ "GET/addresses-with-keys.json"
+ )
+ dispatcher.mockFromAssets(
+ "POST", "/payments/v4/subscription",
+ "POST/payments/v4/subscription-mail-plus-google-managed.json"
+ )
+
+ ActivityScenario.launch(MainActivity::class.java)
+
+ AddAccountRobot()
+ .signIn()
+ .username(testUsername)
+ .password(testPassword)
+ .signIn()
+
+ CoreexampleRobot()
+ .plansUpgrade()
+ .toggleExpandPlan(TestPlan.Plus)
+ .selectPlan(TestPlan.Plus)
+ .apply {
+ verify {
+ googleIAPElementsDisplayed()
+ payWithGoogleButtonIsClickable()
+ }
+ }
+ .payWithGPay()
+ .plansCurrent()
+ .verify {
+ currentPlanDetailsDisplayed()
+ planDetailsDisplayed(TestPlan.Plus)
+ }
+ }
+}
\ No newline at end of file
diff --git a/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/uitests/giap/SignupWithGoogleIapTests.kt b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/uitests/giap/SignupWithGoogleIapTests.kt
new file mode 100644
index 000000000..c91468d8c
--- /dev/null
+++ b/coreexample/src/androidTestMock/kotlin/me/proton/core/test/android/uitests/giap/SignupWithGoogleIapTests.kt
@@ -0,0 +1,226 @@
+/*
+ * 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 .
+ */
+
+package me.proton.core.test.android.uitests.giap
+
+import android.content.Context
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClient.BillingResponseCode
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import me.proton.android.core.coreexample.CoreExampleLogger
+import me.proton.core.auth.presentation.ui.AddAccountActivity
+import me.proton.core.network.data.di.BaseProtonApiUrl
+import me.proton.core.plan.presentation.entity.PlanInput
+import me.proton.core.plan.presentation.ui.StartPlanChooser
+import me.proton.core.plan.presentation.ui.UpgradeActivity
+import me.proton.core.test.android.TestWebServerDispatcher
+import me.proton.core.test.android.mocks.FakeBillingClientFactory
+import me.proton.core.test.android.mocks.mockBillingClientSuccess
+import me.proton.core.test.android.mocks.mockStartConnection
+import me.proton.core.test.android.robots.auth.AddAccountRobot
+import me.proton.core.test.android.robots.auth.signup.RecoveryMethodsRobot
+import me.proton.core.test.android.robots.auth.signup.SignupFinishedRobot
+import me.proton.core.test.android.robots.payments.GoogleIAPRobot
+import me.proton.core.test.android.robots.plans.SelectPlanRobot
+import me.proton.core.util.kotlin.CoreLogger
+import okhttp3.HttpUrl
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Rule
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import me.proton.core.paymentiap.presentation.R as PaymentIapR
+import me.proton.core.test.android.plugins.data.Plan as TestPlan
+
+@HiltAndroidTest
+class SignupWithGoogleIapTests {
+ private val testUsername = "test-mock-936"
+ private val testPassword = "password"
+
+ @get:Rule(order = Rule.DEFAULT_ORDER - 1)
+ val hiltAndroidRule = HiltAndroidRule(this)
+
+ @BindValue
+ @BaseProtonApiUrl
+ lateinit var baseProtonApiUrl: HttpUrl
+
+ @Inject
+ lateinit var billingClientFactory: FakeBillingClientFactory
+
+ private val appContext: Context get() = ApplicationProvider.getApplicationContext()
+ private val billingClient: BillingClient get() = billingClientFactory.billingClient
+ private lateinit var dispatcher: TestWebServerDispatcher
+ private lateinit var webServer: MockWebServer
+
+ @BeforeTest
+ fun setUp() {
+ Timber.plant(Timber.DebugTree())
+ CoreLogger.set(CoreExampleLogger())
+
+ dispatcher = TestWebServerDispatcher()
+ webServer = MockWebServer().apply {
+ dispatcher = this@SignupWithGoogleIapTests.dispatcher
+ }
+ baseProtonApiUrl = webServer.url("/")
+
+ hiltAndroidRule.inject()
+ }
+
+ @AfterTest
+ fun tearDown() {
+ webServer.shutdown()
+ }
+
+ @Test
+ fun googleBillingNotAvailable() {
+ billingClient.mockStartConnection(BillingResponseCode.BILLING_UNAVAILABLE)
+
+ val intent = StartPlanChooser().createIntent(appContext, PlanInput())
+ ActivityScenario.launch(intent)
+
+ SelectPlanRobot()
+ .toggleExpandPlan(TestPlan.Plus)
+ .selectPlan(TestPlan.Plus)
+ .verify {
+ errorSnackbarDisplayed(PaymentIapR.string.payments_iap_error_billing_client_unavailable)
+ }
+ }
+
+ @Test
+ fun signUpAndSubscribeGiapOnly() {
+ billingClient.mockBillingClientSuccess { billingClientFactory.listeners }
+
+ dispatcher.mockFromAssets(
+ "GET", "/payments/v4/status/google",
+ "GET/payments/v4/status/google-iap-only.json"
+ )
+ dispatcher.mockFromAssets(
+ "POST", "/payments/v4/subscription",
+ "POST/payments/v4/subscription-mail-plus-google-managed.json"
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/users",
+ "GET/users-with-keys-subscribed.json"
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/addresses",
+ "GET/addresses-with-keys.json"
+ )
+
+ ActivityScenario.launch(AddAccountActivity::class.java)
+ AddAccountRobot()
+ .createAccount()
+ .setUsername(testUsername)
+ .setAndConfirmPassword(testPassword)
+ .skip()
+ .skipConfirm()
+ .toggleExpandPlan(TestPlan.Plus)
+ .selectPlan(TestPlan.Plus)
+ .payWithGPay()
+ .verify {
+ signupFinishedDisplayed()
+ }
+ }
+
+ @Test
+ fun signUpAndSubscribeAllPaymentProviders() {
+ billingClient.mockBillingClientSuccess { billingClientFactory.listeners }
+
+ dispatcher.mockFromAssets(
+ "GET", "/users",
+ "GET/users-with-keys-subscribed.json"
+ )
+ dispatcher.mockFromAssets(
+ "POST", "/payments/v4/subscription",
+ "POST/payments/v4/subscription-mail-plus-google-managed.json"
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/addresses",
+ "GET/addresses-with-keys.json"
+ )
+
+ ActivityScenario.launch(AddAccountActivity::class.java)
+ AddAccountRobot()
+ .createAccount()
+ .setUsername(testUsername)
+ .setAndConfirmPassword(testPassword)
+ .skip()
+ .skipConfirm()
+ .toggleExpandPlan(TestPlan.Plus)
+ .selectPlan(TestPlan.Plus)
+ .payWithGPay()
+ .verify {
+ signupFinishedDisplayed()
+ }
+ }
+
+ @Test
+ fun switchPaymentOptions() {
+ billingClient.mockBillingClientSuccess { billingClientFactory.listeners }
+
+ val intent = StartPlanChooser().createIntent(appContext, PlanInput())
+ ActivityScenario.launch(intent)
+
+ SelectPlanRobot()
+ .toggleExpandPlan(TestPlan.Plus)
+ .selectPlan(TestPlan.Plus)
+ .apply { verify { googleIAPElementsDisplayed() } }
+ .switchPaymentProvider()
+ .apply { verify { addCreditCardElementsDisplayed() } }
+ .switchPaymentProvider()
+ .apply { verify { googleIAPElementsDisplayed() } }
+ .back()
+ .verify {
+ planDetailsDisplayed(TestPlan.Plus)
+ canSelectPlan(TestPlan.Plus)
+ }
+ }
+
+ @Test
+ fun createFreeAccountWithoutPaymentProviders() {
+ dispatcher.mockFromAssets(
+ "GET", "/payments/v4/status/google",
+ "GET/payments/v4/status/google-all-disabled.json"
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/users",
+ "GET/users-with-keys-subscribed.json"
+ )
+ dispatcher.mockFromAssets(
+ "GET", "/addresses",
+ "GET/addresses-with-keys.json"
+ )
+
+ ActivityScenario.launch(AddAccountActivity::class.java)
+ AddAccountRobot()
+ .createAccount()
+ .setUsername(testUsername)
+ .setAndConfirmPassword(testPassword)
+ .skip()
+ .skipConfirm()
+ .verify {
+ signupFinishedDisplayed()
+ }
+ }
+}
diff --git a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/di/ApplicationModule.kt b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/di/ApplicationModule.kt
index bbef56888..bd5d955f5 100644
--- a/coreexample/src/main/kotlin/me/proton/android/core/coreexample/di/ApplicationModule.kt
+++ b/coreexample/src/main/kotlin/me/proton/android/core/coreexample/di/ApplicationModule.kt
@@ -58,7 +58,11 @@ object ApplicationModule {
@Singleton
fun provideCoreExampleRepository(apiProvider: ApiProvider): CoreExampleRepository =
CoreExampleRepository(apiProvider)
+}
+@Module
+@InstallIn(SingletonComponent::class)
+object WorkManagerModule {
@Provides
@Singleton
fun provideWorkManager(@ApplicationContext context: Context): WorkManager =
diff --git a/coreexample/src/mock/AndroidManifest.xml b/coreexample/src/mock/AndroidManifest.xml
new file mode 100644
index 000000000..6a5e6e9cf
--- /dev/null
+++ b/coreexample/src/mock/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/coreexample/src/mock/res/xml/network_security_config.xml b/coreexample/src/mock/res/xml/network_security_config.xml
new file mode 100644
index 000000000..7fd2a5d3f
--- /dev/null
+++ b/coreexample/src/mock/res/xml/network_security_config.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/payment-iap/dagger/api/payment-iap-dagger.api b/payment-iap/dagger/api/payment-iap-dagger.api
index 62a7483e9..11756d35a 100644
--- a/payment-iap/dagger/api/payment-iap-dagger.api
+++ b/payment-iap/dagger/api/payment-iap-dagger.api
@@ -1,3 +1,7 @@
+public class hilt_aggregated_deps/_me_proton_core_paymentiap_dagger_CorePaymentIapBillingModule {
+ public fun ()V
+}
+
public class hilt_aggregated_deps/_me_proton_core_paymentiap_dagger_CorePaymentIapModule {
public fun ()V
}
@@ -9,9 +13,12 @@ public final class me/proton/core/paymentiap/dagger/BuildConfig {
public fun ()V
}
+public abstract interface class me/proton/core/paymentiap/dagger/CorePaymentIapBillingModule {
+ public abstract fun bindBillingClientProvider (Lme/proton/core/paymentiap/data/BillingClientFactoryImpl;)Lme/proton/core/paymentiap/domain/BillingClientFactory;
+}
+
public abstract interface class me/proton/core/paymentiap/dagger/CorePaymentIapModule {
public abstract fun bindAcknowledgeGooglePlayPurchase (Lme/proton/core/paymentiap/data/usecase/AcknowledgeGooglePlayPurchaseImpl;)Lme/proton/core/payment/domain/usecase/AcknowledgeGooglePlayPurchase;
- public abstract fun bindBillingClientProvider (Lme/proton/core/paymentiap/data/BillingClientFactoryImpl;)Lme/proton/core/paymentiap/domain/BillingClientFactory;
public abstract fun bindFindUnredeemedGooglePurchase (Lme/proton/core/paymentiap/data/usecase/FindUnacknowledgedGooglePurchaseImpl;)Lme/proton/core/payment/domain/usecase/FindUnacknowledgedGooglePurchase;
public abstract fun bindGoogleBillingRepository (Lme/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImpl;)Lme/proton/core/paymentiap/domain/repository/GoogleBillingRepository;
}
diff --git a/payment-iap/dagger/src/main/kotlin/me/proton/core/paymentiap/dagger/CorePaymentIapModule.kt b/payment-iap/dagger/src/main/kotlin/me/proton/core/paymentiap/dagger/CorePaymentIapModule.kt
index 4a96e688f..1658cde29 100644
--- a/payment-iap/dagger/src/main/kotlin/me/proton/core/paymentiap/dagger/CorePaymentIapModule.kt
+++ b/payment-iap/dagger/src/main/kotlin/me/proton/core/paymentiap/dagger/CorePaymentIapModule.kt
@@ -33,10 +33,14 @@ import me.proton.core.paymentiap.domain.repository.GoogleBillingRepository
@Module
@InstallIn(SingletonComponent::class)
-public interface CorePaymentIapModule {
+public interface CorePaymentIapBillingModule {
@Binds
public fun bindBillingClientProvider(impl: BillingClientFactoryImpl): BillingClientFactory
+}
+@Module
+@InstallIn(SingletonComponent::class)
+public interface CorePaymentIapModule {
@Binds
public fun bindGoogleBillingRepository(impl: GoogleBillingRepositoryImpl): GoogleBillingRepository
diff --git a/payment-iap/data/src/main/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImpl.kt b/payment-iap/data/src/main/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImpl.kt
index 220b3ab0a..a04ba5d7f 100644
--- a/payment-iap/data/src/main/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImpl.kt
+++ b/payment-iap/data/src/main/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImpl.kt
@@ -52,7 +52,6 @@ import me.proton.core.util.kotlin.DispatcherProvider
import javax.inject.Inject
public class GoogleBillingRepositoryImpl @Inject internal constructor(
- billingClientFactory: BillingClientFactory,
connectedBillingClientFactory: ConnectedBillingClientFactory,
dispatcherProvider: DispatcherProvider,
) : GoogleBillingRepository {
@@ -67,7 +66,7 @@ public class GoogleBillingRepositoryImpl @Inject internal constructor(
}
}
- private val connectedBillingClient = connectedBillingClientFactory(billingClientFactory(purchasesUpdatedListener))
+ private val connectedBillingClient = connectedBillingClientFactory(purchasesUpdatedListener)
override suspend fun acknowledgePurchase(purchaseToken: GooglePurchaseToken) {
val params = AcknowledgePurchaseParams.newBuilder()
@@ -120,13 +119,15 @@ public class GoogleBillingRepositoryImpl @Inject internal constructor(
@AssistedFactory
internal interface ConnectedBillingClientFactory {
- operator fun invoke(billingClient: BillingClient): ConnectedBillingClient
+ operator fun invoke(purchasesUpdatedListener: PurchasesUpdatedListener): ConnectedBillingClient
}
/** Manages access to [BillingClient], ensuring we are connected, before calling any of its methods. */
internal class ConnectedBillingClient @AssistedInject constructor(
- @Assisted private val billingClient: BillingClient
+ billingClientFactory: BillingClientFactory,
+ @Assisted private val purchasesUpdatedListener: PurchasesUpdatedListener
) : BillingClientStateListener {
+ private val billingClient = billingClientFactory(purchasesUpdatedListener)
private val connectionState = MutableStateFlow(BillingClientConnectionState.Idle)
fun destroy() {
diff --git a/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/ConnectedBillingClientTest.kt b/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/ConnectedBillingClientTest.kt
index dd6fc8bbf..957932b64 100644
--- a/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/ConnectedBillingClientTest.kt
+++ b/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/ConnectedBillingClientTest.kt
@@ -44,7 +44,7 @@ internal class ConnectedBillingClientTest {
@BeforeTest
fun setUp() {
billingClient = mockk(relaxed = true)
- tested = ConnectedBillingClient(billingClient)
+ tested = ConnectedBillingClient({ billingClient }, mockk())
}
@Test
diff --git a/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImplTest.kt b/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImplTest.kt
index ac8208cc3..f598e1ec9 100644
--- a/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImplTest.kt
+++ b/payment-iap/data/src/test/kotlin/me/proton/core/paymentiap/data/repository/GoogleBillingRepositoryImplTest.kt
@@ -48,7 +48,7 @@ internal class GoogleBillingRepositoryImplTest {
fun setUp() {
billingClientFactory = FakeBillingClientFactory()
factory = FakeConnectedBillingClientFactory()
- tested = GoogleBillingRepositoryImpl(billingClientFactory, factory, TestDispatcherProvider)
+ tested = GoogleBillingRepositoryImpl(factory, TestDispatcherProvider)
}
@AfterTest
@@ -125,5 +125,6 @@ private class FakeBillingClientFactory : BillingClientFactory {
private class FakeConnectedBillingClientFactory : ConnectedBillingClientFactory {
val connectedBillingClient: ConnectedBillingClient = mockk(relaxed = true)
- override fun invoke(billingClient: BillingClient): ConnectedBillingClient = connectedBillingClient
+ override fun invoke(purchasesUpdatedListener: PurchasesUpdatedListener): ConnectedBillingClient =
+ connectedBillingClient
}
diff --git a/payment-iap/domain/src/main/kotlin/me/proton/core/paymentiap/domain/BillingClientFactory.kt b/payment-iap/domain/src/main/kotlin/me/proton/core/paymentiap/domain/BillingClientFactory.kt
index 9fcd177c3..25b7bcf10 100644
--- a/payment-iap/domain/src/main/kotlin/me/proton/core/paymentiap/domain/BillingClientFactory.kt
+++ b/payment-iap/domain/src/main/kotlin/me/proton/core/paymentiap/domain/BillingClientFactory.kt
@@ -21,6 +21,6 @@ package me.proton.core.paymentiap.domain
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.PurchasesUpdatedListener
-public interface BillingClientFactory {
+public fun interface BillingClientFactory {
public operator fun invoke(purchasesUpdatedListener: PurchasesUpdatedListener): BillingClient
}
diff --git a/plugins/core/src/main/kotlin/Dependencies.kt b/plugins/core/src/main/kotlin/Dependencies.kt
index 74be65a9a..7852735e3 100644
--- a/plugins/core/src/main/kotlin/Dependencies.kt
+++ b/plugins/core/src/main/kotlin/Dependencies.kt
@@ -58,7 +58,7 @@ public val DependencyHandler.`compose-material`: Any
public val DependencyHandler.`compose-material3`: Any
get() = compose("material3", version = `material3 version`)
public val DependencyHandler.`compose-material-icons-core`: Any
- get() = compose("material", moduleSuffix="icons-core")
+ get() = compose("material", moduleSuffix = "icons-core")
public val DependencyHandler.`compose-runtime`: Any
get() = compose("runtime")
public val DependencyHandler.`compose-ui`: Any
@@ -151,7 +151,7 @@ public val DependencyHandler.`startup-runtime`: Any
public val DependencyHandler.`serialization-core`: Any
get() = serialization("core")
public val DependencyHandler.`desugar-jdk-libs`: Any
- get() = dependency("com.android.tools", module="desugar_jdk_libs") version `desugar_jdk_libs version`
+ get() = dependency("com.android.tools", module = "desugar_jdk_libs") version `desugar_jdk_libs version`
// region accessors
public fun DependencyHandler.lint(moduleSuffix: String? = null, version: String = `android-tools version`): Any =
@@ -163,6 +163,8 @@ public val DependencyHandler.`android-test-core-ktx`: Any
get() = dependency("androidx.test", module = "core-ktx") version `android-test version`
public val DependencyHandler.`androidx-test-monitor`: Any
get() = androidx("test", module = "monitor") version `androidx-test-monitor version`
+public val DependencyHandler.`androidx-test-orchestrator`: Any
+ get() = androidx("test", module = "orchestrator") version `androidx-test-orchestrator version`
public val DependencyHandler.`espresso-contrib`: Any
get() = androidx("test.espresso", module = "espresso-contrib") version `espresso version`
public val DependencyHandler.`espresso-idling-resource`: Any
diff --git a/plugins/core/src/main/kotlin/versionsConfig.kt b/plugins/core/src/main/kotlin/versionsConfig.kt
index 4a8beff97..1cbec00c7 100644
--- a/plugins/core/src/main/kotlin/versionsConfig.kt
+++ b/plugins/core/src/main/kotlin/versionsConfig.kt
@@ -116,12 +116,13 @@ public const val `desugar_jdk_libs version`: String = "1.1.5"
// endregion
// region Tests
-public const val `androidx-test-monitor version`: String = "1.5.0"
-public const val `hamcrest version`: String = "2.2"
-public const val `uiautomator version`: String = "2.2.0" // Released: Oct 25, 2018
-public const val `preference version`: String = "1.1.1" // Released: Apr 15, 2020
-public const val `json-simple version`: String = "1.1.1" // Released: Mar 21, 2012
-public const val `turbine version`: String = "0.7.0"
-public const val `junit version`: String = "4.13.2"
-public const val `junit-ktx version`: String = "1.1.3"
+public const val `androidx-test-monitor version`: String = "1.5.0"
+public const val `androidx-test-orchestrator version`: String = "1.4.1"
+public const val `hamcrest version`: String = "2.2"
+public const val `uiautomator version`: String = "2.2.0" // Released: Oct 25, 2018
+public const val `preference version`: String = "1.1.1" // Released: Apr 15, 2020
+public const val `json-simple version`: String = "1.1.1" // Released: Mar 21, 2012
+public const val `turbine version`: String = "0.7.0"
+public const val `junit version`: String = "4.13.2"
+public const val `junit-ktx version`: String = "1.1.3"
// endregion
diff --git a/test/android/instrumented/api/test-android-instrumented.api b/test/android/instrumented/api/test-android-instrumented.api
index 67dc792d8..a0c479bbb 100644
--- a/test/android/instrumented/api/test-android-instrumented.api
+++ b/test/android/instrumented/api/test-android-instrumented.api
@@ -1055,6 +1055,14 @@ public final class me/proton/core/test/android/robots/payments/ExistingPaymentMe
public final class me/proton/core/test/android/robots/payments/GoogleIAPRobot : me/proton/core/test/android/robots/payments/PaymentRobot {
public fun ()V
+ public final fun switchPaymentProvider ()Lme/proton/core/test/android/robots/payments/GoogleIAPRobot;
+ public final fun verify (Lkotlin/jvm/functions/Function1;)Lme/proton/core/test/android/robots/payments/GoogleIAPRobot$Verify;
+}
+
+public final class me/proton/core/test/android/robots/payments/GoogleIAPRobot$Verify : me/proton/core/test/android/robots/payments/PaymentRobot$Verify {
+ public fun ()V
+ public final fun payWithCardButtonIsNotVisible ()V
+ public final fun payWithGoogleButtonIsClickable ()V
}
public class me/proton/core/test/android/robots/payments/PaymentRobot : me/proton/core/test/android/robots/CoreRobot {
@@ -1062,12 +1070,13 @@ public class me/proton/core/test/android/robots/payments/PaymentRobot : me/proto
public final fun verify (Lkotlin/jvm/functions/Function1;)Lme/proton/core/test/android/robots/payments/PaymentRobot$Verify;
}
-public final class me/proton/core/test/android/robots/payments/PaymentRobot$Verify : me/proton/core/test/android/robots/CoreVerify {
+public class me/proton/core/test/android/robots/payments/PaymentRobot$Verify : me/proton/core/test/android/robots/CoreVerify {
public fun ()V
public final fun addCreditCardElementsDisplayed ()V
public final fun billingDetailsDisplayed (Lme/proton/core/test/android/plugins/data/Plan;Ljava/lang/String;Z)V
public final fun googleIAPElementsDisplayed ()V
public final fun googlePaymentMethodDisplayed (Ljava/lang/String;)V
+ public final fun nextPaymentProviderButtonDisplayed ()V
public final fun paymentMethodDisplayed (Ljava/lang/String;Ljava/lang/String;)V
}
@@ -1086,6 +1095,7 @@ public final class me/proton/core/test/android/robots/plans/SelectPlanRobot$Veri
public static synthetic fun billingCycleIs$default (Lme/proton/core/test/android/robots/plans/SelectPlanRobot$Verify;Lme/proton/core/test/android/plugins/data/Plan;Lme/proton/core/test/android/plugins/data/BillingCycle;Lme/proton/core/test/android/plugins/data/Currency;ILjava/lang/Object;)V
public final fun canSelectPlan (Lme/proton/core/test/android/plugins/data/Plan;)V
public final fun canUpgradeToPlan (Lme/proton/core/test/android/plugins/data/Plan;)V
+ public final fun currentPlanDetailsDisplayed ()V
public final fun planCycleDisplayed (Ljava/lang/String;)V
public final fun planDetailsDisplayed (Lme/proton/core/test/android/plugins/data/Plan;)V
public final fun planDetailsDisplayedInsideRecyclerView (Lme/proton/core/test/android/plugins/data/Plan;)V
diff --git a/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/GoogleIAPRobot.kt b/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/GoogleIAPRobot.kt
index 1f92fe7a5..3b70ed32c 100644
--- a/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/GoogleIAPRobot.kt
+++ b/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/GoogleIAPRobot.kt
@@ -18,10 +18,30 @@
package me.proton.core.test.android.robots.payments
+import me.proton.core.payment.presentation.R
+
/**
* [GoogleIAPRobot] class contains new credit card addition actions and verifications implementation
*/
class GoogleIAPRobot : PaymentRobot() {
- inline fun payWithGPay() = pay()
+ inline fun payWithGPay(): T = clickElement(R.id.gPayButton)
+
+ fun switchPaymentProvider(): GoogleIAPRobot = clickElement(R.id.nextPaymentProviderButton)
+
+ @Suppress("FINAL_UPPER_BOUND")
+ fun verify(block: Verify.() -> Unit) = Verify().apply(block)
+
+ class Verify : PaymentRobot.Verify() {
+ fun payWithCardButtonIsNotVisible() {
+ view.withId(R.id.payButton).checkNotDisplayed()
+ }
+
+ fun payWithGoogleButtonIsClickable() {
+ view.withId(R.id.gPayButton)
+ .checkDisplayed()
+ .isEnabled()
+ .isClickable()
+ }
+ }
}
diff --git a/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/PaymentRobot.kt b/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/PaymentRobot.kt
index d67ff20db..d406d0b7e 100644
--- a/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/PaymentRobot.kt
+++ b/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/payments/PaymentRobot.kt
@@ -35,7 +35,7 @@ open class PaymentRobot : CoreRobot() {
*/
inline fun pay(): T = clickElement(R.id.payButton)
- class Verify : CoreVerify() {
+ open class Verify : CoreVerify() {
fun billingDetailsDisplayed(
plan: Plan,
currency: String,
@@ -75,12 +75,15 @@ open class PaymentRobot : CoreRobot() {
fun googleIAPElementsDisplayed() {
arrayOf(
me.proton.core.paymentiap.presentation.R.id.termsConditionsInfoText,
- me.proton.core.paymentiap.presentation.R.id.priceSurchargeInfoText,
- R.id.nextPaymentProviderButton
+ me.proton.core.paymentiap.presentation.R.id.priceSurchargeInfoText
).forEach {
view.withId(it).checkDisplayed()
}
}
+
+ fun nextPaymentProviderButtonDisplayed() {
+ view.withId(R.id.nextPaymentProviderButton).checkDisplayed()
+ }
}
inline fun verify(block: Verify.() -> Unit) = Verify().apply(block)
diff --git a/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/plans/SelectPlanRobot.kt b/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/plans/SelectPlanRobot.kt
index cf6dbb080..0c58c7ea1 100644
--- a/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/plans/SelectPlanRobot.kt
+++ b/test/android/instrumented/src/main/kotlin/me/proton/core/test/android/robots/plans/SelectPlanRobot.kt
@@ -132,6 +132,10 @@ class SelectPlanRobot : CoreRobot() {
view.withId(R.id.currentPlan).checkNotDisplayed()
}
+ fun currentPlanDetailsDisplayed() {
+ view.withId(R.id.currentPlan).checkDisplayed()
+ }
+
fun canSelectPlan(plan: Plan) {
scrollToPlan(plan)