From 95e559e7db7316c7a5c72fe4929d1b71f5e8cfe3 Mon Sep 17 00:00:00 2001 From: Denys Zelenchuk Date: Mon, 3 Feb 2025 13:40:41 +0000 Subject: [PATCH] chore: Introduced mock-proxy module with SDK and pull mock files plugin. --- README.md | 1 + .../scenarios/auth_scenario1/auth_mock1.json | 27 +++ .../scenarios/auth_scenario1/auth_mock2.json | 24 +++ .../auth_scenario1/auth_scenario.json | 5 + .../libs/lint/ProtonIssueRegistry.kt | 3 +- .../UsesCleartextTrafficManifestDetector.kt | 41 ++++ plugins/CHANGELOG.md | 2 +- plugins/README.md | 12 +- plugins/core/src/main/kotlin/modules.kt | 1 + plugins/mock-proxy/build.gradle.kts | 56 +++++ .../kotlin/mockproxy/MockFilesPullerPlugin.kt | 87 ++++++++ plugins/settings.gradle.kts | 3 +- test/mock-proxy/.gitignore | 1 + test/mock-proxy/README.md | 76 +++++++ test/mock-proxy/build.gradle.kts | 45 ++++ .../scenarios/auth_scenario1/auth_mock1.json | 29 +++ .../scenarios/auth_scenario1/auth_mock2.json | 26 +++ .../auth_scenario1/auth_scenario.json | 5 + .../core/test/mockproxy/MockProxyTest.kt | 136 ++++++++++++ test/mock-proxy/src/main/AndroidManifest.xml | 28 +++ .../scenarios/auth_scenario1/auth_mock1.json | 27 +++ .../scenarios/auth_scenario1/auth_mock2.json | 24 +++ .../scenarios/auth_scenario1/auth_mock3.json | 19 ++ .../auth_scenario1/auth_scenario.json | 5 + .../proton/core/test/mockproxy/Constants.kt | 24 +++ .../me/proton/core/test/mockproxy/MockApi.kt | 56 +++++ .../proton/core/test/mockproxy/MockClient.kt | 193 ++++++++++++++++++ .../proton/core/test/mockproxy/MockObjects.kt | 114 +++++++++++ .../proton/core/test/mockproxy/MockParser.kt | 162 +++++++++++++++ .../core/test/mockproxy/TrafficParameters.kt | 48 +++++ test/rule/build.gradle.kts | 1 + .../me/proton/core/test/rule/MockTestRule.kt | 64 ++++++ .../core/test/rule/QuarkTestDataRule.kt | 16 +- .../core/test/rule/annotation/MockTest.kt | 36 ++++ 34 files changed, 1388 insertions(+), 9 deletions(-) create mode 100644 coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock1.json create mode 100644 coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock2.json create mode 100644 coreexample/src/debug/assets/scenarios/auth_scenario1/auth_scenario.json create mode 100644 lint/src/main/kotlin/ch/protonmail/libs/lint/detectors/UsesCleartextTrafficManifestDetector.kt create mode 100644 plugins/mock-proxy/build.gradle.kts create mode 100644 plugins/mock-proxy/src/main/kotlin/mockproxy/MockFilesPullerPlugin.kt create mode 100644 test/mock-proxy/.gitignore create mode 100644 test/mock-proxy/README.md create mode 100644 test/mock-proxy/build.gradle.kts create mode 100644 test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock1.json create mode 100644 test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock2.json create mode 100644 test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_scenario.json create mode 100644 test/mock-proxy/src/androidTest/kotlin/me/proton/core/test/mockproxy/MockProxyTest.kt create mode 100644 test/mock-proxy/src/main/AndroidManifest.xml create mode 100644 test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock1.json create mode 100644 test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock2.json create mode 100644 test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock3.json create mode 100644 test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_scenario.json create mode 100644 test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/Constants.kt create mode 100644 test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockApi.kt create mode 100644 test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockClient.kt create mode 100644 test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockObjects.kt create mode 100644 test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockParser.kt create mode 100644 test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/TrafficParameters.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/MockTestRule.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/MockTest.kt diff --git a/README.md b/README.md index d766a363d..64edbc459 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,7 @@ Core libraries coordinates can be found under [coordinates section](#coordinates | me.proton.core:test-android | | me.proton.core:test-android-instrumented | | me.proton.core:test-kotlin | +| me.proton.core:test-mock-proxy | | me.proton.core:test-quark | | me.proton.core:test-performance | | me.proton.core:test-rule | diff --git a/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock1.json b/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock1.json new file mode 100644 index 000000000..1f779e260 --- /dev/null +++ b/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock1.json @@ -0,0 +1,27 @@ +{ + "request": { + "exactUrl": [ + "/api/v4/test1" + ], + "method": "post" + }, + "response": { + "statusCode": 200, + "headers": { + "header1": "header1-1", + "header2": "header2-2" + }, + "body": { + "Code": 1000, + "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nY+6iVlkGjwe9tBlF3aDN8dXR4W7U3mX0r4RjfwxckmX3YSwfKCm/zM33b0As8mS8A5kpMSnvGs+E/Mc8CNOPQW6Oulxw4e0prdW9gEZEUCSJA3z02HWGk13/7zjmdzyWiU8yiWtmFga6i8GwfXUyjufS8T+1UYMvCa/HE8R6i2HNgfhhIQ0lKWqcO9DpAK/icPZUUtjVE4Xh2IDLDvjoQbiRJEo4bU6zMYEGcqy0g7a1Vz0IvSvQRFeMHxRuqtV8NtuDzISSm/gj/4DylPmOUQBdt1VPD4UKhPHLfS/MKt7MJz4cdnc6vrIsY2QDVCZvNa0zckaARjanz3MLPOwd/A==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAABgEAEA/sDprqlXbiuE+RFWe9HW\n3u9jxwTXpPVXHtkkFCILzokBAOvXQ7yKDrAEXH4f045IyVoFwnhBc6OcPD/x\ncmCxAokC\n=JC9y\n-----END PGP SIGNATURE-----\n", + "ServerEphemeral": "S0R4Lfi2t4Bu+StgU39uXpZEYPfG/4vo2WuCGB7f0CfLFfg2he1+HvKCatly/n89KBQx+ei7uoVvD+iDxlaKzPyMvJ+ZiLB73QNaDwsWesnEYmA8ozJgoCRRJf+IBwx7fZAMD/mp9T5J6xscPULmLdFVNrAGjX+v99iTpVanXbFDZ7I/YA3gxjtcrgwyUuQkpkoHyVkuKnodH206e5cYX3szyKHzI48MsZy+UJ2gfuzCT1sYY43+tC1AKet7DNbthfDN9ERMfjWTUghba54DHoscy9GiVHibjD33xoQXpIeubMvI87mrv5Uu0voBYKsJ8TfZIhs/I0j+Xtrmskc5xQ==", + "Version": 4, + "Salt": "DnJh2bj2RBChNQ==", + "SRPSession": "2f34224721344fdb5a62e6f441a496a7" + } + }, + "meta": { + "test": "test case name2", + "description": "test case description 2" + } +} \ No newline at end of file diff --git a/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock2.json b/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock2.json new file mode 100644 index 000000000..a2eb8ee87 --- /dev/null +++ b/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_mock2.json @@ -0,0 +1,24 @@ +{ + "request": { + "exactUrl": [ + "/api/v4/test2" + ], + "method": "post" + }, + "response": { + "statusCode": 200, + "headers": {"header11": "header11-11", "header22": "header22-22"}, + "body": { + "Code": 1000, + "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nY+6iVlkGjwe9tBlF3aDN8dXR4W7U3mX0r4RjfwxckmX3YSwfKCm/zM33b0As8mS8A5kpMSnvGs+E/Mc8CNOPQW6Oulxw4e0prdW9gEZEUCSJA3z02HWGk13/7zjmdzyWiU8yiWtmFga6i8GwfXUyjufS8T+1UYMvCa/HE8R6i2HNgfhhIQ0lKWqcO9DpAK/icPZUUtjVE4Xh2IDLDvjoQbiRJEo4bU6zMYEGcqy0g7a1Vz0IvSvQRFeMHxRuqtV8NtuDzISSm/gj/4DylPmOUQBdt1VPD4UKhPHLfS/MKt7MJz4cdnc6vrIsY2QDVCZvNa0zckaARjanz3MLPOwd/A==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAABgEAEA/sDprqlXbiuE+RFWe9HW\n3u9jxwTXpPVXHtkkFCILzokBAOvXQ7yKDrAEXH4f045IyVoFwnhBc6OcPD/x\ncmCxAokC\n=JC9y\n-----END PGP SIGNATURE-----\n", + "ServerEphemeral": "S0R4Lfi2t4Bu+StgU39uXpZEYPfG/4vo2WuCGB7f0CfLFfg2he1+HvKCatly/n89KBQx+ei7uoVvD+iDxlaKzPyMvJ+ZiLB73QNaDwsWesnEYmA8ozJgoCRRJf+IBwx7fZAMD/mp9T5J6xscPULmLdFVNrAGjX+v99iTpVanXbFDZ7I/YA3gxjtcrgwyUuQkpkoHyVkuKnodH206e5cYX3szyKHzI48MsZy+UJ2gfuzCT1sYY43+tC1AKet7DNbthfDN9ERMfjWTUghba54DHoscy9GiVHibjD33xoQXpIeubMvI87mrv5Uu0voBYKsJ8TfZIhs/I0j+Xtrmskc5xQ==", + "Version": 4, + "Salt": "DnJh2bj2RBChNQ==", + "SRPSession": "2f34224721344fdb5a62e6f441a496a7" + } + }, + "meta": { + "test": "test case name2", + "description": "test case description 2" + } +} \ No newline at end of file diff --git a/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_scenario.json b/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_scenario.json new file mode 100644 index 000000000..8ac6745fa --- /dev/null +++ b/coreexample/src/debug/assets/scenarios/auth_scenario1/auth_scenario.json @@ -0,0 +1,5 @@ +{ + "description": "scenario description", + "updateFile": false, + "mockFiles": ["scenarios/auth_scenario1/auth_mock1.json", "scenarios/auth_scenario1/auth_mock2.json"] +} \ No newline at end of file diff --git a/lint/src/main/kotlin/ch/protonmail/libs/lint/ProtonIssueRegistry.kt b/lint/src/main/kotlin/ch/protonmail/libs/lint/ProtonIssueRegistry.kt index 15f0165ca..773b5467f 100644 --- a/lint/src/main/kotlin/ch/protonmail/libs/lint/ProtonIssueRegistry.kt +++ b/lint/src/main/kotlin/ch/protonmail/libs/lint/ProtonIssueRegistry.kt @@ -29,6 +29,7 @@ class ProtonIssueRegistry : IssueRegistry() { override val issues: List get() = listOf( HardcodedCoroutineDispatcherDetector, NoSerialNameAnnotationDetector, - NotConstantStringDetector + NotConstantStringDetector, + UsesCleartextTrafficManifestDetector ).flatMap { it.ISSUES } } diff --git a/lint/src/main/kotlin/ch/protonmail/libs/lint/detectors/UsesCleartextTrafficManifestDetector.kt b/lint/src/main/kotlin/ch/protonmail/libs/lint/detectors/UsesCleartextTrafficManifestDetector.kt new file mode 100644 index 000000000..520adb2b2 --- /dev/null +++ b/lint/src/main/kotlin/ch/protonmail/libs/lint/detectors/UsesCleartextTrafficManifestDetector.kt @@ -0,0 +1,41 @@ +package ch.protonmail.libs.lint.detectors + +import com.android.tools.lint.detector.api.* +import org.w3c.dom.Element + +class UsesCleartextTrafficManifestDetector : Detector(), XmlScanner { + + override fun getApplicableElements(): Collection { + // Look for and tags + return listOf("uses-permission", "application") + } + + override fun visitElement(context: XmlContext, element: Element) { + if (element.tagName == "application") { + val cleartextTraffic = element.getAttribute("android:usesCleartextTraffic") + if (cleartextTraffic == "true") { + context.report( + ISSUE_FORBIDDEN_USES_CLEARTEXT, + element, + context.getLocation(element), + "`usesCleartextTraffic=\"true\"` must not be included in the release manifest." + ) + } + } + } + + companion object { + val ISSUE_FORBIDDEN_USES_CLEARTEXT = Issue.create( + id = "ForbiddenUsesCleartextTraffic", + briefDescription = "Forbidden usesCleartextTraffic in release build", + explanation = "Cleartext traffic should not be enabled in the release manifest.", + category = Category.SECURITY, + priority = 9, + severity = Severity.ERROR, + implementation = Implementation( + ForbiddenManifestCheck::class.java, + Scope.MANIFEST_SCOPE + ) + ) + } +} diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 9ffb13846..b86b18042 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes -- Added product flavor dimension support to environment config plugin. +- Added product flavor dimension support to environment config plugin. ## [1.3.0] diff --git a/plugins/README.md b/plugins/README.md index be6eb75eb..72a6b7984 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -152,8 +152,6 @@ Use internally in core project to orchestrate [core libraries publication](../RE This plugin should be applied to either build flavor or application build type android extension in `build.gradle.kts` file. -* - Generates `build/generated/source/envConfig/{flavor}/{buildType}/EnvironmentConfigurationDefaults.java` similarly to `BuildConfig.java`. Generated class is then added to source directories and can be accessed at runtime @@ -162,6 +160,16 @@ Generated class is then added to source directories and can be accessed at runti * By default (if no configuration provided in build.gradle.kts) generates a default production config (`api.proton.me`) +## Mock-proxy file puller plugin + +- Plugin id: `me.proton.core.gradle-plugins.mock-proxy` +- Published on MavenCentral. + +Allows to automatically pull recorded mock files from local mock-proxy server. +In order to use it define below environment variables in your `local.properties` file: +1. `MOCK_PROXY_RECORD_DIR=/path-to-local-mock-roxy-repository` +2. `PROJECT_MOCK_FILES_DIR=/path-to-mock-files-dir-in-android-project` + ```kotlin build.gradle.kts diff --git a/plugins/core/src/main/kotlin/modules.kt b/plugins/core/src/main/kotlin/modules.kt index b09e9a60e..c27fc5669 100644 --- a/plugins/core/src/main/kotlin/modules.kt +++ b/plugins/core/src/main/kotlin/modules.kt @@ -39,6 +39,7 @@ public object Module { public const val androidTest: String = "$test:test-android" public const val androidInstrumentedTest: String = "$androidTest:test-android-instrumented" public const val quark: String = "$test:test-quark" + public const val mockProxy: String = "$test:test-mock-proxy" public const val testPerformance: String = "$test:test-performance" public const val testRule: String = "$test:test-rule" // endregion diff --git a/plugins/mock-proxy/build.gradle.kts b/plugins/mock-proxy/build.gradle.kts new file mode 100644 index 000000000..d7b929f58 --- /dev/null +++ b/plugins/mock-proxy/build.gradle.kts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 Proton Technologies AG + * This file is part of Proton Technologies 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 . + */ +import org.gradle.kotlin.dsl.`java-gradle-plugin` +import org.gradle.kotlin.dsl.`kotlin-dsl` +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.gradlePlugin +import org.gradle.kotlin.dsl.implementation +import org.gradle.kotlin.dsl.kotlin +import org.gradle.kotlin.dsl.repositories + +plugins { + `kotlin-dsl` + kotlin("jvm") + `java-gradle-plugin` +} + +publishOption.shouldBePublishedAsPlugin = true + +gradlePlugin { + plugins { + create("mockFilesPuller") { + id = "me.proton.core.gradle-plugins.mock-proxy" + displayName = "Proton pull recordings mock-proxy plugin." + description = "Pulls recorded files from and to defined locations." + implementationClass = "mockproxy.MockFilesPullerPlugin" + } + } +} + +repositories { + google() + mavenCentral() + maven("https://plugins.gradle.org/m2/") +} + +dependencies { + implementation(gradleApi()) + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin") + compileOnly(libs.android.gradle) + api(libs.easyGradle.androidDsl) +} diff --git a/plugins/mock-proxy/src/main/kotlin/mockproxy/MockFilesPullerPlugin.kt b/plugins/mock-proxy/src/main/kotlin/mockproxy/MockFilesPullerPlugin.kt new file mode 100644 index 000000000..99994fb73 --- /dev/null +++ b/plugins/mock-proxy/src/main/kotlin/mockproxy/MockFilesPullerPlugin.kt @@ -0,0 +1,87 @@ +package mockproxy + +import com.android.build.gradle.internal.coverage.JacocoReportTask.JacocoReportWorkerAction.Companion.logger +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.util.Properties + +class MockFilesPullerPlugin : Plugin { + override fun apply(project: Project) { + val properties = Properties().apply { + runCatching { + load(project.rootDir.resolve("local.properties").inputStream()) + }.recoverCatching { + load(project.rootDir.resolve("private.properties").inputStream()) + }.getOrElse { throwable -> + // Provide empty properties to allow the app to be built without secrets + logger.warn( + "MockFilePullerPlugin: local.properties or private.properties " + + "file not found. Proceeding with empty properties." + + "\n Message ${throwable.message}" + ) + Properties() + } + } + + project.tasks.register("pullMockFiles", PullMockFilesTask::class.java) { + // Default setup: destination directory within the project + destinationDir = + project.layout.projectDirectory.dir( + properties.getProperty("PROJECT_MOCK_FILES_DIR") + ).asFile + sourceDir = + project.layout.projectDirectory.dir( + properties.getProperty("MOCK_PROXY_RECORD_DIR") + ).asFile + } + } +} + + +abstract class PullMockFilesTask : DefaultTask() { + @InputDirectory + lateinit var sourceDir: File + + @OutputDirectory + lateinit var destinationDir: File + + init { + group = "Mock" + description = "Pulls mock files from a source directory to the destination directory." + } + + @TaskAction + fun pullMockFiles() { + logger.lifecycle( + "MockFilePullerPlugin: Pulling mock files from $sourceDir to $destinationDir" + ) + + if (!sourceDir.exists()) { + throw IllegalArgumentException("Source directory does not exist: $sourceDir") + } + destinationDir.mkdirs() + sourceDir.walkTopDown().forEach { file -> + val destinationFile = File(destinationDir, file.relativeTo(sourceDir).path) + if (file.isDirectory) { + destinationFile.mkdirs() + } else { + Files.copy( + file.toPath(), + destinationFile.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + logger.lifecycle( + "MockFilePullerPlugin: Copied: ${file.absolutePath} ->" + + " ${destinationFile.absolutePath}" + ) + } + } + } +} diff --git a/plugins/settings.gradle.kts b/plugins/settings.gradle.kts index 639cad52e..185e26547 100644 --- a/plugins/settings.gradle.kts +++ b/plugins/settings.gradle.kts @@ -28,7 +28,8 @@ include( "tests", "include-core-build", "publish-core-libraries", - "environment-configuration" + "environment-configuration", + "mock-proxy" ) pluginManagement { diff --git a/test/mock-proxy/.gitignore b/test/mock-proxy/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/test/mock-proxy/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/test/mock-proxy/README.md b/test/mock-proxy/README.md new file mode 100644 index 000000000..e9e26c299 --- /dev/null +++ b/test/mock-proxy/README.md @@ -0,0 +1,76 @@ +# Mock Proxy SDK + +Mock proxy SDK allows to control mock proxy server parameters from inside your test. + +Supported features: +1. Single route mock. +2. Scenario mocking (collection of routes). +3. Control bandwidth. +4. Control latency. + +## Start using +To start using SDK create an instance of `MockClient(baseUrl: String)`: +```kotlin +val mockClient = MockClient(baseUrl) +``` +Here `baseUrl` can be an atlas environment or the local host URL. + +If you run tests locally using emulator use Android localhost baseUrl value from `Constants`: +```kotlin +public const val EMULATOR_LOCALHOST: String = "http://10.0.2.2:3001/" +``` + +`MockClient` instance creation with pre-defined local host value for Android emulator: +```kotlin +val mockClient = MockClient(EMULATOR_LOCALHOST) +``` + +## Mock single route +SDK assumes mock files are stored in app or test assets. + +Mock single route from app assets: +```kotlin +mockClient.setStaticMockFromAppAssets(routeFilePath) +``` + +Mock single route from test assets: +```kotlin +mockClient.setStaticMockFromTestAssets(routeFilePath) +``` +## Mock scenario +Mock scenario route from app assets (both app and test assets): +```kotlin +mockClient.setScenarioFromAssets(scenarioFilePath) +``` + +Reset scenario from app assets: +```kotlin +mockClient.resetScenarioFromAppAssets(scenarioFilePath) +``` + +Reset scenario from test assets: +```kotlin +mockClient.resetScenarioFromTestAssets(scenarioFilePath) +``` + +## Get, set and reset latency + +```kotlin + +val latencyInfo = mockClient.getLatency() +mockClient.setLatency(LatencyLevel.HIGH) +mockClient.resetLatency() +``` + +## Get, set and reset bandwidth + +```kotlin +val bandwidth = mockClient.getBandwidth() +mockClient.setBandwidth(BandwidthLimit._4G) +mockClient.resetBandwidth() +``` + +## Resetting the state (reset all) +```kotlin +mockClient.resetAllMocks() +``` \ No newline at end of file diff --git a/test/mock-proxy/build.gradle.kts b/test/mock-proxy/build.gradle.kts new file mode 100644 index 000000000..4ef2cb0eb --- /dev/null +++ b/test/mock-proxy/build.gradle.kts @@ -0,0 +1,45 @@ +import studio.forface.easygradle.dsl.android.`android-test-runner` +import studio.forface.easygradle.dsl.android.androidTestImplementation +import studio.forface.easygradle.dsl.android.`hilt-android-testing` +import studio.forface.easygradle.dsl.android.retrofit +import studio.forface.easygradle.dsl.android.`retrofit-kotlin-serialization` +import studio.forface.easygradle.dsl.implementation +import studio.forface.easygradle.dsl.`kotlin-test` +import studio.forface.easygradle.dsl.`kotlin-test-junit` +import studio.forface.easygradle.dsl.`serialization-json` +import studio.forface.easygradle.dsl.squareup +import studio.forface.easygradle.dsl.version + +plugins { + protonAndroidLibrary + kotlin("plugin.serialization") + jacoco +} + +protonCoverage.disabled.set(true) +publishOption.shouldBePublishedAsLib = true + +android { + namespace = "me.proton.core.test.mockproxy" +} + +dependencies { + implementation( + junit, + retrofit, + okhttp, + `android-test-runner`, + `hilt-android-testing`, + `kotlin-test`, + `kotlin-test-junit`, + `retrofit-kotlin-serialization`, + `serialization-json`, + squareup("okhttp3", "okhttp-tls") version `okHttp version`, + ) + + androidTestImplementation( + `kotlin-test` + ) + + androidTestUtil(`androidx-test-orchestrator`) +} diff --git a/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock1.json b/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock1.json new file mode 100644 index 000000000..c4db2900a --- /dev/null +++ b/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock1.json @@ -0,0 +1,29 @@ +{ + "request": { + "exactUrl": [ + "/api/v4/test1" + ], + "method": "post" + }, + "response": { + "statusCode": 200, + "headers": { + "header1": "header1-1", + "header2": "header2-2" + }, + "body": { + "Code": 1000, + "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nY+6iVlkGjwe9tBlF3aDN8dXR4W7U3mX0r4RjfwxckmX3YSwfKCm/zM33b0As8mS8A5kpMSnvGs+E/Mc8CNOPQW6Oulxw4e0prdW9gEZEUCSJA3z02HWGk13/7zjmdzyWiU8yiWtmFga6i8GwfXUyjufS8T+1UYMvCa/HE8R6i2HNgfhhIQ0lKWqcO9DpAK/icPZUUtjVE4Xh2IDLDvjoQbiRJEo4bU6zMYEGcqy0g7a1Vz0IvSvQRFeMHxRuqtV8NtuDzISSm/gj/4DylPmOUQBdt1VPD4UKhPHLfS/MKt7MJz4cdnc6vrIsY2QDVCZvNa0zckaARjanz3MLPOwd/A==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAABgEAEA/sDprqlXbiuE+RFWe9HW\n3u9jxwTXpPVXHtkkFCILzokBAOvXQ7yKDrAEXH4f045IyVoFwnhBc6OcPD/x\ncmCxAokC\n=JC9y\n-----END PGP SIGNATURE-----\n", + "ServerEphemeral": "S0R4Lfi2t4Bu+StgU39uXpZEYPfG/4vo2WuCGB7f0CfLFfg2he1+HvKCatly/n89KBQx+ei7uoVvD+iDxlaKzPyMvJ+ZiLB73QNaDwsWesnEYmA8ozJgoCRRJf+IBwx7fZAMD/mp9T5J6xscPULmLdFVNrAGjX+v99iTpVanXbFDZ7I/YA3gxjtcrgwyUuQkpkoHyVkuKnodH206e5cYX3szyKHzI48MsZy+UJ2gfuzCT1sYY43+tC1AKet7DNbthfDN9ERMfjWTUghba54DHoscy9GiVHibjD33xoQXpIeubMvI87mrv5Uu0voBYKsJ8TfZIhs/I0j+Xtrmskc5xQ==", + "Version": 4, + "Salt": "DnJh2bj2RBChNQ==", + "SRPSession": "2f34224721344fdb5a62e6f441a496a7" + } + }, + "meta": { + "test": "test case name2", + "description": "test case description 2" + }, + "name": "test mock 1", + "enabled": false +} \ No newline at end of file diff --git a/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock2.json b/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock2.json new file mode 100644 index 000000000..3a65650fe --- /dev/null +++ b/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_mock2.json @@ -0,0 +1,26 @@ +{ + "request": { + "exactUrl": [ + "/api/v4/test2" + ], + "method": "post" + }, + "response": { + "statusCode": 200, + "headers": {"header11": "header11-11", "header22": "header22-22"}, + "body": { + "Code": 1000, + "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nY+6iVlkGjwe9tBlF3aDN8dXR4W7U3mX0r4RjfwxckmX3YSwfKCm/zM33b0As8mS8A5kpMSnvGs+E/Mc8CNOPQW6Oulxw4e0prdW9gEZEUCSJA3z02HWGk13/7zjmdzyWiU8yiWtmFga6i8GwfXUyjufS8T+1UYMvCa/HE8R6i2HNgfhhIQ0lKWqcO9DpAK/icPZUUtjVE4Xh2IDLDvjoQbiRJEo4bU6zMYEGcqy0g7a1Vz0IvSvQRFeMHxRuqtV8NtuDzISSm/gj/4DylPmOUQBdt1VPD4UKhPHLfS/MKt7MJz4cdnc6vrIsY2QDVCZvNa0zckaARjanz3MLPOwd/A==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAABgEAEA/sDprqlXbiuE+RFWe9HW\n3u9jxwTXpPVXHtkkFCILzokBAOvXQ7yKDrAEXH4f045IyVoFwnhBc6OcPD/x\ncmCxAokC\n=JC9y\n-----END PGP SIGNATURE-----\n", + "ServerEphemeral": "S0R4Lfi2t4Bu+StgU39uXpZEYPfG/4vo2WuCGB7f0CfLFfg2he1+HvKCatly/n89KBQx+ei7uoVvD+iDxlaKzPyMvJ+ZiLB73QNaDwsWesnEYmA8ozJgoCRRJf+IBwx7fZAMD/mp9T5J6xscPULmLdFVNrAGjX+v99iTpVanXbFDZ7I/YA3gxjtcrgwyUuQkpkoHyVkuKnodH206e5cYX3szyKHzI48MsZy+UJ2gfuzCT1sYY43+tC1AKet7DNbthfDN9ERMfjWTUghba54DHoscy9GiVHibjD33xoQXpIeubMvI87mrv5Uu0voBYKsJ8TfZIhs/I0j+Xtrmskc5xQ==", + "Version": 4, + "Salt": "DnJh2bj2RBChNQ==", + "SRPSession": "2f34224721344fdb5a62e6f441a496a7" + } + }, + "meta": { + "test": "test case name2", + "description": "test case description 2" + }, + "name": "test mock 2", + "enabled": false +} \ No newline at end of file diff --git a/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_scenario.json b/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_scenario.json new file mode 100644 index 000000000..8ac6745fa --- /dev/null +++ b/test/mock-proxy/src/androidTest/assets/scenarios/auth_scenario1/auth_scenario.json @@ -0,0 +1,5 @@ +{ + "description": "scenario description", + "updateFile": false, + "mockFiles": ["scenarios/auth_scenario1/auth_mock1.json", "scenarios/auth_scenario1/auth_mock2.json"] +} \ No newline at end of file diff --git a/test/mock-proxy/src/androidTest/kotlin/me/proton/core/test/mockproxy/MockProxyTest.kt b/test/mock-proxy/src/androidTest/kotlin/me/proton/core/test/mockproxy/MockProxyTest.kt new file mode 100644 index 000000000..4dad0b60e --- /dev/null +++ b/test/mock-proxy/src/androidTest/kotlin/me/proton/core/test/mockproxy/MockProxyTest.kt @@ -0,0 +1,136 @@ +package me.proton.core.test.mockproxy + +import me.proton.core.test.mockproxy.Constants.EMULATOR_LOCALHOST +import org.junit.After +import org.junit.Assert.assertThrows +import org.junit.Test +import java.io.FileNotFoundException +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MockProxyTest { + + private val mockClient = MockClient(EMULATOR_LOCALHOST) + + @After + fun afterTest() { + mockClient.resetAllMocks() + } + + @Test + fun testScenarioMockingFromTestAssets() { + val scenarioFilePath = "scenarios/auth_scenario1/auth_scenario.json" + val staticMocksResponseList: List = + mockClient.setScenarioFromAssets(scenarioFilePath) + assertTrue(staticMocksResponseList.count() == 2) + } + + @Test + fun testScenarioMockingFromAppAssets() { + val scenarioFilePath = "scenarios/auth_scenario1/auth_scenario.json" + mockClient.setLatency(LatencyLevel.EXTREME) + val staticMocksResponseList: List = + mockClient.setScenarioFromAssets(scenarioFilePath) + assertTrue(staticMocksResponseList.count() == 2) + } + + @Test + fun testResetScenarioMockingFromAppAssets() { + val scenarioFilePath = "scenarios/auth_scenario1/auth_scenario.json" + mockClient.setLatency(LatencyLevel.EXTREME) + var staticMocksResponseList: List = + mockClient.setScenarioFromAssets(scenarioFilePath) + assert(staticMocksResponseList.count() == 2) + staticMocksResponseList.forEach { staticMock -> + assertTrue(staticMock.enabled, "Static mock should be enabled but it is not.") + } + staticMocksResponseList = mockClient.resetScenarioFromAssets(scenarioFilePath) + staticMocksResponseList.forEach { staticMock -> + assertFalse(staticMock.enabled, "Static mock should be disabled but it is not.") + } + } + + @Test + fun testResetScenarioMockingFromTestAssets() { + val scenarioFilePath = "scenarios/auth_scenario1/auth_scenario.json" + mockClient.setLatency(LatencyLevel.EXTREME) + var staticMocksResponseList: List = + mockClient.setScenarioFromAssets(scenarioFilePath) + assert(staticMocksResponseList.count() == 2) + staticMocksResponseList.forEach { staticMock -> + assertTrue(staticMock.enabled, "Static mock should be enabled but it is not.") + } + staticMocksResponseList = mockClient.resetScenarioFromAssets(scenarioFilePath) + staticMocksResponseList.forEach { staticMock -> + assertFalse(staticMock.enabled, "Static mock should be disabled but it is not.") + } + } + + @Test + fun testSingleAuthRouteMockingFromAppAssets() { + val routeFilePath = "scenarios/auth_scenario1/auth_mock1.json" + val staticMockResponse: MockObject = mockClient.setStaticMockFromAssets(routeFilePath) + assertTrue(staticMockResponse.response.statusCode == 200) + } + + @Test + fun testSingleAuthRouteMockingFromTestAssets() { + val routeFilePath = "scenarios/auth_scenario1/auth_mock1.json" + val staticMockResponse: MockObject = mockClient.setStaticMockFromAssets(routeFilePath) + assertTrue(staticMockResponse.response.statusCode == 200) + } + + @Test + fun testFailNotExistingFileLoadFromTestAssets() { + val notExistingFilePath = "scenarios/auth_scenario1/not_exists.json" + val exception = assertThrows(FileNotFoundException::class.java) { + mockClient.setStaticMockFromAssets(notExistingFilePath) + } + assertTrue(exception.message?.contains(notExistingFilePath) == true) + } + + @Test + fun testFailNotExistingFileLoadFromAppAssets() { + val notExistingFilePath = "scenarios/auth_scenario1/not_exists.json" + val exception = assertThrows(FileNotFoundException::class.java) { + mockClient.setStaticMockFromAssets(notExistingFilePath) + } + assertTrue(exception.message?.contains(notExistingFilePath) == true) + } + + @Test + fun testSetLatency() { + mockClient.setLatency(LatencyLevel.HIGH) + val latencyInfo = mockClient.getLatency() + assertTrue(latencyInfo.enabled && latencyInfo.latency == LatencyLevel.HIGH.latencyMs) + } + + @Test + fun testSetAndResetLatency() { + mockClient.setLatency(LatencyLevel.HIGH) + var latencyInfo = mockClient.getLatency() + assertTrue(latencyInfo.enabled && latencyInfo.latency == LatencyLevel.HIGH.latencyMs) + + mockClient.resetLatency() + latencyInfo = mockClient.getLatency() + assertTrue(!latencyInfo.enabled && latencyInfo.latency == LatencyLevel.NONE.latencyMs) + } + + @Test + fun testSetBandwidth() { + mockClient.setBandwidth(BandwidthLimit.NONE) + val bandwidthInfo = mockClient.getBandwidth() + assertTrue(bandwidthInfo.enabled && bandwidthInfo.limit == BandwidthLimit.NONE.speedKbps) + } + + @Test + fun testSetAndResetBandwidth() { + mockClient.setBandwidth(BandwidthLimit._4G) + var bandwidthInfo = mockClient.getBandwidth() + assert(bandwidthInfo.enabled && bandwidthInfo.limit == BandwidthLimit._4G.speedKbps) + + mockClient.resetBandwidth() + bandwidthInfo = mockClient.getBandwidth() + assertTrue(!bandwidthInfo.enabled && bandwidthInfo.limit == BandwidthLimit.NONE.speedKbps) + } +} diff --git a/test/mock-proxy/src/main/AndroidManifest.xml b/test/mock-proxy/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d7e801d1d --- /dev/null +++ b/test/mock-proxy/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock1.json b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock1.json new file mode 100644 index 000000000..1f779e260 --- /dev/null +++ b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock1.json @@ -0,0 +1,27 @@ +{ + "request": { + "exactUrl": [ + "/api/v4/test1" + ], + "method": "post" + }, + "response": { + "statusCode": 200, + "headers": { + "header1": "header1-1", + "header2": "header2-2" + }, + "body": { + "Code": 1000, + "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nY+6iVlkGjwe9tBlF3aDN8dXR4W7U3mX0r4RjfwxckmX3YSwfKCm/zM33b0As8mS8A5kpMSnvGs+E/Mc8CNOPQW6Oulxw4e0prdW9gEZEUCSJA3z02HWGk13/7zjmdzyWiU8yiWtmFga6i8GwfXUyjufS8T+1UYMvCa/HE8R6i2HNgfhhIQ0lKWqcO9DpAK/icPZUUtjVE4Xh2IDLDvjoQbiRJEo4bU6zMYEGcqy0g7a1Vz0IvSvQRFeMHxRuqtV8NtuDzISSm/gj/4DylPmOUQBdt1VPD4UKhPHLfS/MKt7MJz4cdnc6vrIsY2QDVCZvNa0zckaARjanz3MLPOwd/A==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAABgEAEA/sDprqlXbiuE+RFWe9HW\n3u9jxwTXpPVXHtkkFCILzokBAOvXQ7yKDrAEXH4f045IyVoFwnhBc6OcPD/x\ncmCxAokC\n=JC9y\n-----END PGP SIGNATURE-----\n", + "ServerEphemeral": "S0R4Lfi2t4Bu+StgU39uXpZEYPfG/4vo2WuCGB7f0CfLFfg2he1+HvKCatly/n89KBQx+ei7uoVvD+iDxlaKzPyMvJ+ZiLB73QNaDwsWesnEYmA8ozJgoCRRJf+IBwx7fZAMD/mp9T5J6xscPULmLdFVNrAGjX+v99iTpVanXbFDZ7I/YA3gxjtcrgwyUuQkpkoHyVkuKnodH206e5cYX3szyKHzI48MsZy+UJ2gfuzCT1sYY43+tC1AKet7DNbthfDN9ERMfjWTUghba54DHoscy9GiVHibjD33xoQXpIeubMvI87mrv5Uu0voBYKsJ8TfZIhs/I0j+Xtrmskc5xQ==", + "Version": 4, + "Salt": "DnJh2bj2RBChNQ==", + "SRPSession": "2f34224721344fdb5a62e6f441a496a7" + } + }, + "meta": { + "test": "test case name2", + "description": "test case description 2" + } +} \ No newline at end of file diff --git a/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock2.json b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock2.json new file mode 100644 index 000000000..a2eb8ee87 --- /dev/null +++ b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock2.json @@ -0,0 +1,24 @@ +{ + "request": { + "exactUrl": [ + "/api/v4/test2" + ], + "method": "post" + }, + "response": { + "statusCode": 200, + "headers": {"header11": "header11-11", "header22": "header22-22"}, + "body": { + "Code": 1000, + "Modulus": "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nY+6iVlkGjwe9tBlF3aDN8dXR4W7U3mX0r4RjfwxckmX3YSwfKCm/zM33b0As8mS8A5kpMSnvGs+E/Mc8CNOPQW6Oulxw4e0prdW9gEZEUCSJA3z02HWGk13/7zjmdzyWiU8yiWtmFga6i8GwfXUyjufS8T+1UYMvCa/HE8R6i2HNgfhhIQ0lKWqcO9DpAK/icPZUUtjVE4Xh2IDLDvjoQbiRJEo4bU6zMYEGcqy0g7a1Vz0IvSvQRFeMHxRuqtV8NtuDzISSm/gj/4DylPmOUQBdt1VPD4UKhPHLfS/MKt7MJz4cdnc6vrIsY2QDVCZvNa0zckaARjanz3MLPOwd/A==\n-----BEGIN PGP SIGNATURE-----\nVersion: ProtonMail\nComment: https://protonmail.com\n\nwl4EARYIABAFAlwB1j8JEDUFhcTpUY8mAABgEAEA/sDprqlXbiuE+RFWe9HW\n3u9jxwTXpPVXHtkkFCILzokBAOvXQ7yKDrAEXH4f045IyVoFwnhBc6OcPD/x\ncmCxAokC\n=JC9y\n-----END PGP SIGNATURE-----\n", + "ServerEphemeral": "S0R4Lfi2t4Bu+StgU39uXpZEYPfG/4vo2WuCGB7f0CfLFfg2he1+HvKCatly/n89KBQx+ei7uoVvD+iDxlaKzPyMvJ+ZiLB73QNaDwsWesnEYmA8ozJgoCRRJf+IBwx7fZAMD/mp9T5J6xscPULmLdFVNrAGjX+v99iTpVanXbFDZ7I/YA3gxjtcrgwyUuQkpkoHyVkuKnodH206e5cYX3szyKHzI48MsZy+UJ2gfuzCT1sYY43+tC1AKet7DNbthfDN9ERMfjWTUghba54DHoscy9GiVHibjD33xoQXpIeubMvI87mrv5Uu0voBYKsJ8TfZIhs/I0j+Xtrmskc5xQ==", + "Version": 4, + "Salt": "DnJh2bj2RBChNQ==", + "SRPSession": "2f34224721344fdb5a62e6f441a496a7" + } + }, + "meta": { + "test": "test case name2", + "description": "test case description 2" + } +} \ No newline at end of file diff --git a/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock3.json b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock3.json new file mode 100644 index 000000000..eed02cabc --- /dev/null +++ b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_mock3.json @@ -0,0 +1,19 @@ +{ + "name": "loginMock", + "enabled": true, + "request": { + "exactUrl": [ + "/api/core/v4/auth" + ] + }, + "response": { + "statusCode": 422, + "body": { + "Code": 8002, + "Error": "Incorrect login credentials. Please do not try again.", + "Details": {}, + "exception": "Proton\\Http\\Exceptions\\UnprocessableEntityException", + "message": "Incorrect login credentials. Please do not try again." + } + } +} \ No newline at end of file diff --git a/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_scenario.json b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_scenario.json new file mode 100644 index 000000000..8ac6745fa --- /dev/null +++ b/test/mock-proxy/src/main/assets/scenarios/auth_scenario1/auth_scenario.json @@ -0,0 +1,5 @@ +{ + "description": "scenario description", + "updateFile": false, + "mockFiles": ["scenarios/auth_scenario1/auth_mock1.json", "scenarios/auth_scenario1/auth_mock2.json"] +} \ No newline at end of file diff --git a/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/Constants.kt b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/Constants.kt new file mode 100644 index 000000000..3f30b961c --- /dev/null +++ b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/Constants.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 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.mockproxy + +public object Constants { + public const val EMULATOR_LOCALHOST: String = "http://10.0.2.2:3001/" + public const val MOCK_PROXY_TAG: String = "Mock-proxy" +} diff --git a/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockApi.kt b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockApi.kt new file mode 100644 index 000000000..5dc8e9115 --- /dev/null +++ b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockApi.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 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.mockproxy + +import retrofit2.http.* + +internal interface MockApi { + + /** Single route mock **/ + @POST("/mock/route/static") + suspend fun setStaticMock(@Body mocks: MockObject): MockObject + + /** List of routes to mock **/ + @GET("/mock/routes/static") + suspend fun getStaticMocks(): List + @POST("/mock/routes/static") + suspend fun setStaticMocks(@Body mocks: List): List + + /** List of routes to mock dynamically **/ + @GET("/mock/routes/dynamic") + suspend fun getDynamicMocks(): List + @POST("/mock/routes/dynamic") + suspend fun setDynamicMocks(@Body mocks: DynamicMockObject): DynamicMockObject + + /** Latency endpoints **/ + @GET("/mock/latency") + suspend fun getLatency(): LatencyObject + @POST("/mock/latency") + suspend fun setLatency(@Body latencyInfo: LatencyObject): LatencyObject + + /** Bandwidth endpoints **/ + @GET("/mock/bandwidth") + suspend fun getBandwidth(): BandwidthObject + @POST("/mock/bandwidth") + suspend fun setBandwidth(@Body bandwidthInfo: BandwidthObject): BandwidthObject + + /** Reset endpoints **/ + @POST("/mock/reset/all") + suspend fun resetAllMocks(): ResponseMessage +} diff --git a/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockClient.kt b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockClient.kt new file mode 100644 index 000000000..8f507316e --- /dev/null +++ b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockClient.kt @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2024 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.mockproxy + +import android.util.Log +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit + +public class MockClient(private val baseUrl: String, private val proxyToken: String = "") { + + private val retrofitBuilder: Retrofit.Builder = Retrofit.Builder() + .baseUrl(baseUrl) + .client( + OkHttpClient.Builder() + .addInterceptor { chain -> + val request = chain.request() + .newBuilder() + .addHeader("x-atlas-secret", proxyToken) + .build() + chain.proceed(request) + } + .build() + ) + + private var mockApi: MockApi = retrofitBuilder + .addConverterFactory(Json.asConverterFactory(MimeType.JSON.value.toMediaType())) + .build() + .create(MockApi::class.java) + + public fun setSRPMock( + shouldEnable: Boolean = false, + password: String + ) { + Log.d( + "MockProxy", + "Executing SRP request with baseUrl: ${this.baseUrl} and proxyToken: ${this.proxyToken}" + ) + val srpMockList = DynamicMockObject( + name = "loginWithSrp", + enabled = shouldEnable, + parameters = mapOf("password" to password) + ) + runBlocking { mockApi.setDynamicMocks(srpMockList) } + } + + public fun setRecording( + shouldEnable: Boolean = false, + recordingDirPath: String + ) { + Log.d( + "MockProxy", + "Executing set recording request with baseUrl: ${this.baseUrl} and proxyToken: ${this.proxyToken}" + ) + val mock = DynamicMockObject( + name = "recordAll", + enabled = shouldEnable, + updateFile = true, + parameters = mapOf("mockDirectory" to recordingDirPath) + ) + runBlocking { + mockApi.setDynamicMocks(mock) + } + } + + public fun setScenarioFromAssets( + scenarioFilePath: String, + shouldUpdateFile: Boolean = false + ): List { + val staticMocksList = + MockParser.parseScenarioFileFromAssets(scenarioFilePath, true, shouldUpdateFile) + return this.setStaticMocks(staticMocksList) + } + + public fun resetScenarioFromAssets(scenarioFilePath: String): List { + val staticMocksList = MockParser.parseScenarioFileFromAssets(scenarioFilePath, false) + lateinit var mockObjectResponse: List + runBlocking { mockObjectResponse = mockApi.setStaticMocks(staticMocksList) } + return mockObjectResponse + } + + public fun getScenarioDirPath(scenarioFilePath: String): String = + MockParser.getScenarioDirectoryOrThrow(scenarioFilePath) + + public fun getStaticMocks(): List { + lateinit var mockObjectResponse: List + runBlocking { mockObjectResponse = mockApi.getStaticMocks() } + return mockObjectResponse + } + + public fun setStaticMockFromAssets(filePath: String): MockObject { + val staticMockFile = MockParser.parseMockFileFromAssets(filePath) + lateinit var mockObjectResponse: MockObject + runBlocking { mockObjectResponse = mockApi.setStaticMock(staticMockFile) } + return mockObjectResponse + } + + private fun setStaticMocks(mockRoutesList: List): List { + lateinit var mockObjectResponse: List + runBlocking { mockObjectResponse = mockApi.setStaticMocks(mockRoutesList) } + return mockObjectResponse + } + + public fun setStaticMock(staticMock: MockObject): MockObject { + lateinit var mockObjectResponse: MockObject + runBlocking { mockObjectResponse = mockApi.setStaticMock(staticMock) } + return mockObjectResponse + } + + public fun resetStaticMock(staticMock: MockObject): MockObject { + staticMock.enabled = false + lateinit var mockObjectResponse: MockObject + runBlocking { mockObjectResponse = mockApi.setStaticMock(staticMock) } + return mockObjectResponse + } + + public fun getLatency(): LatencyObject { + lateinit var mockObjectResponse: LatencyObject + runBlocking { mockObjectResponse = mockApi.getLatency() } + return mockObjectResponse + } + + public fun setLatency(latencyLevel: LatencyLevel): LatencyObject { + val latencyInfo = LatencyObject( + enabled = true, + latency = latencyLevel.latencyMs + ) + lateinit var mockObjectResponse: LatencyObject + runBlocking { mockObjectResponse = mockApi.setLatency(latencyInfo) } + return mockObjectResponse + } + + public fun resetLatency(): LatencyObject { + val latencyInfo = LatencyObject( + enabled = false, + latency = LatencyLevel.NONE.latencyMs + ) + lateinit var mockObjectResponse: LatencyObject + runBlocking { mockObjectResponse = mockApi.setLatency(latencyInfo) } + return mockObjectResponse + } + + public fun getBandwidth(): BandwidthObject { + lateinit var mockObjectResponse: BandwidthObject + runBlocking { mockObjectResponse = mockApi.getBandwidth() } + return mockObjectResponse + } + + public fun setBandwidth(bandwidthLimit: BandwidthLimit = BandwidthLimit.NONE): BandwidthObject { + val bandwidthInfo = BandwidthObject( + enabled = true, + limit = bandwidthLimit.speedKbps + ) + lateinit var mockObjectResponse: BandwidthObject + runBlocking { mockObjectResponse = mockApi.setBandwidth(bandwidthInfo) } + return mockObjectResponse + } + + public fun resetBandwidth(): BandwidthObject { + val bandwidthInfo = BandwidthObject( + enabled = false, + limit = BandwidthLimit.NONE.speedKbps + ) + lateinit var mockObjectResponse: BandwidthObject + runBlocking { mockObjectResponse = mockApi.setBandwidth(bandwidthInfo) } + return mockObjectResponse + } + + public fun resetAllMocks(): String { + lateinit var mockObjectResponse: String + runBlocking { mockObjectResponse = mockApi.resetAllMocks().message } + return mockObjectResponse + } +} diff --git a/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockObjects.kt b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockObjects.kt new file mode 100644 index 000000000..c3bf48f02 --- /dev/null +++ b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockObjects.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 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.mockproxy + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +public data class MockObject( + var name: String, + var enabled: Boolean, + var updateFile: Boolean? = null, + val isRawFileContent: Boolean = false, + var request: MockFile.RequestData, + var response: MockFile.ResponseData +) + +@Serializable +public data class DynamicMockObject( + var name: String, + var enabled: Boolean, + var description: String? = null, + var updateFile: Boolean? = null, + var parameters: Map? = null, + var mocks: List? = null +) + +@Serializable +public data class DynamicMock( + var request: MockFile.RequestData? = null, + var response: MockFile.ResponseData? = null +) + +@Serializable +public data class ScenarioMockObject( + val scenarioName: String, + var enabled: Boolean, + val updateFile: Boolean? = null +) + +@Serializable +public data class LatencyObject( + var enabled: Boolean, + val latency: Int +) + +@Serializable +public data class BandwidthObject( + var enabled: Boolean, + val limit: Int +) + +@Serializable +public data class ScenarioFileObject( + val description: String, + val updateFile: Boolean, + val mockFiles: List +) + +@Serializable +public data class MockFile( + val request: RequestData, + val response: ResponseData, + val name: String, + var enabled: Boolean, + val meta: MetaData? = null +) { + @Serializable + public data class RequestData( + val exactUrl: List? = listOf(), + val matchUrl: List? = listOf(), + val method: String? = null + ) + + @Serializable + public data class ResponseData( + val headers: Map? = null, + val body: JsonElement? = null, + val statusCode: Int, + ) + + @Serializable + public data class MetaData( + val test: String, + val description: String, + ) +} + +@Serializable +public data class ParsedScenarioFile( + val name: String, + var enabled: Boolean, + val request: MockFile.RequestData, + val response: MockFile.ResponseData +) + +@Serializable +public data class ResponseMessage(val message: String) diff --git a/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockParser.kt b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockParser.kt new file mode 100644 index 000000000..2e2b24e3e --- /dev/null +++ b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/MockParser.kt @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024 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.mockproxy + +import android.content.res.AssetManager +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.serialization.json.Json +import java.io.File +import java.io.FileNotFoundException + +internal object MockParser { + private val testContext + get() = InstrumentationRegistry.getInstrumentation().context + private val testAssetManager + get() = testContext.assets ?: error("Could not load app assets.") + + private fun readFileFromAssets(filePath: String, assetManager: AssetManager): String = + assetManager.open(filePath).bufferedReader().use { it.readText() } + + internal fun parseMockFileFromAssets(mockFilePath: String): MockObject { + val assetManager = getAssetManager(mockFilePath) + return parseAssetFileToStaticMock(mockFilePath, assetManager) + } + + private fun parseAssetFileToStaticMock( + mockFilePath: String, + assetManager: AssetManager, + isEnabled: Boolean = false + ): MockObject { + val mockFile = File(mockFilePath) + val mockFileContent = readFileFromAssets(mockFilePath, assetManager) + val decodedMockFile: MockFile = Json.decodeFromString(mockFileContent) + val fileName = mockFile.nameWithoutExtension + return MockObject( + name = fileName, + enabled = isEnabled, + updateFile = false, + request = decodedMockFile.request, + response = decodedMockFile.response + ) + } + + internal fun getScenarioDirectoryOrThrow( + scenarioFilePath: String + ): String { + val assetManager = getAssetManager(scenarioFilePath) + val fileContent = readFileFromAssets(scenarioFilePath, assetManager) + val scenarioFileObject: ScenarioFileObject = Json.decodeFromString(fileContent) + // Get the first mock file and check if it is a folder + val firstMockFile = scenarioFileObject.mockFiles[0] + val assetList = assetManager.list(firstMockFile) + + if (assetList.isNullOrEmpty()) { + throw IllegalStateException("The first mock file is not a folder: $firstMockFile") + } + return firstMockFile + } + + internal fun parseScenarioFileFromAssets( + scenarioFilePath: String, + isEnabled: Boolean, + shouldUpdateFile: Boolean = false + ): List { + val assetManager = getAssetManager(scenarioFilePath) + return parseAssetFileToListOfStaticMocks( + scenarioFilePath, + assetManager, + isEnabled, + shouldUpdateFile + ) + } + + private fun getAssetManager(scenarioFilePath: String): AssetManager { + val assetManager = runCatching { + readFileFromAssets( + scenarioFilePath, + testAssetManager + ).also { println("Read from testAssetManager") } + testAssetManager + }.getOrElse { + throw FileNotFoundException( + "Failed to read file from both test and app assets. File path: $scenarioFilePath" + ) + } + return assetManager + } + + private fun parseAssetFileToListOfStaticMocks( + scenarioFilePath: String, + assetManager: AssetManager, + isEnabled: Boolean = false, + shouldUpdateFile: Boolean = false + ): List { + val mockRoutesList = mutableListOf() + val fileContent = readFileFromAssets(scenarioFilePath, assetManager) + val scenarioFileObject: ScenarioFileObject = Json.decodeFromString(fileContent) + processMockFiles( + scenarioFileObject.mockFiles, + assetManager, + isEnabled, + shouldUpdateFile, + mockRoutesList + ) + return mockRoutesList + } + + private fun processMockFiles( + mockFiles: List, + assetManager: AssetManager, + isEnabled: Boolean, + shouldUpdateFile: Boolean, + mockRoutesList: MutableList + ) { + mockFiles.forEach { mockFilePath -> + val assetList = assetManager.list(mockFilePath) + if (assetList.isNullOrEmpty()) { + val mockFile = File(mockFilePath) + val mockName = + "${System.currentTimeMillis()}-${mockFile.parentFile?.name}-${mockFile.nameWithoutExtension}" + val mockFileContent = readFileFromAssets(mockFilePath, assetManager) + val decodedMockFile: MockFile = Json.decodeFromString(mockFileContent) + + mockRoutesList.add( + MockObject( + name = mockName, + enabled = isEnabled, + updateFile = shouldUpdateFile, + request = decodedMockFile.request, + response = decodedMockFile.response + ) + ) + } else { + assetList.forEach { nestedFileName -> + val nestedFilePath = "$mockFilePath/$nestedFileName" + processMockFiles( + listOf(nestedFilePath), + assetManager, + isEnabled, + shouldUpdateFile, + mockRoutesList + ) + } + } + } + } +} diff --git a/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/TrafficParameters.kt b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/TrafficParameters.kt new file mode 100644 index 000000000..1e96aa5d4 --- /dev/null +++ b/test/mock-proxy/src/main/kotlin/me/proton/core/test/mockproxy/TrafficParameters.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 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.mockproxy + +public enum class MimeType(public val value: String) { + JSON("application/json"), + IMAGE_SVG("image/svg+xml"), + IMAGE_PNG("image/png"), + OCTET_STREAM("application/octet-stream"), + MULTIPART_FORM_DATA("multipart/form-data"); +} + +public enum class LatencyLevel(public val latencyMs: Int) { + NONE(0), + LOW(50), + MODERATE(150), + HIGH(300), + VERY_HIGH(600), + CRITICAL(1000), + EXTREME(5000), +} + +public enum class BandwidthLimit(public val speedKbps: Int) { + GPRS(56), // Real-world maximum + EDGE(236), // Real-world maximum + _2G(256), // Real-world maximum + _3G(42000), // Theoretical maximum for HSPA+ + _4G(1000000), // 1 Gbps = Theoretical maximum + WIFI(9600000), // WiFi 6 theoretical maximum (9.6 Gbps) + BROADBAND(1000000), // Common high-speed broadband (1 Gbps) + NONE(Int.MAX_VALUE) // Unlimited +} diff --git a/test/rule/build.gradle.kts b/test/rule/build.gradle.kts index 1d4078500..e2ecdc14c 100644 --- a/test/rule/build.gradle.kts +++ b/test/rule/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { project(Module.configurationData), project(Module.configurationDaggerContentResolver), project(Module.quark), + project(Module.mockProxy), project(Module.authDomain), project(Module.authPresentation), `coroutines-android`, diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/MockTestRule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/MockTestRule.kt new file mode 100644 index 000000000..4ab3f33f7 --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/MockTestRule.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 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.rule + +import me.proton.core.test.mockproxy.MockClient +import me.proton.core.test.rule.annotation.MockTest +import me.proton.core.test.rule.annotation.PrepareUser +import me.proton.core.util.kotlin.takeIfNotEmpty +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Responsible for mock-proxy behavior configuration. + * Can act in test replay and record mode based on [shouldRecord] parameter. + * Enables SRP mocking in replay mode. + */ +public class MockTestRule( + private val mockClient: MockClient, + private val shouldRecord: Boolean = false +) : TestRule { + override fun apply(base: Statement, description: Description): Statement { + val mockTest = description.getAnnotation(MockTest::class.java) + ?: description.testClass.getAnnotation(MockTest::class.java) + + mockTest?.let { + mockTest.scenario.let { scenario -> + if (shouldRecord && mockTest.isReference) { + mockClient.setRecording( + true, + mockClient.getScenarioDirPath(scenario) + ) + } else { + description.getAnnotation(PrepareUser::class.java)?.let { user -> + mockClient.setSRPMock( + true, + // Fall back to the default password value as we have to + // pass password to SRP anyway here. + user.userData.password.takeIfNotEmpty() ?: "password" + ) + mockClient.setScenarioFromAssets(scenario) + } + } + } + } + return base + } +} diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt index d8b60df24..660dc8528 100644 --- a/test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt @@ -105,6 +105,8 @@ public class QuarkTestDataRule( prepareUserAnnotations = method?.findAnnotations().orEmpty() paymentMethodAnnotation = method?.findAnnotation() + quarkCommand = getQuarkCommand(environmentConfiguration()) + if (prepareUserAnnotations.isNotEmpty()) { prepareUserAnnotations.forEach { prepareUser -> @@ -140,7 +142,6 @@ public class QuarkTestDataRule( */ var handledUserData = prepareUser.userData.handleUserData() var createdUserResponse: CreateUserQuarkResponse? - quarkCommand = getQuarkCommand(environmentConfiguration()) val userSeedingTime = measureTime { createdUserResponse = prepareUser.annotationTestData.implementation( @@ -300,10 +301,17 @@ public class QuarkTestDataRule( } } - private fun getQuarkCommand(envConfig: EnvironmentConfiguration): QuarkCommand = - QuarkCommand(quarkClient) - .baseUrl("https://${envConfig.host}/api/internal") + private fun getQuarkCommand(envConfig: EnvironmentConfiguration): QuarkCommand { + lateinit var baseUrl: String + if (envConfig.host.contains("10.0.2.2")) { + baseUrl = "http://${envConfig.host}/api/internal" + } else { + baseUrl = "https://${envConfig.host}/api/internal" + } + return QuarkCommand(quarkClient) + .baseUrl(baseUrl) .proxyToken(envConfig.proxyToken) + } public fun getAnnotationProperty(annotation: Annotation, propertyName: String): Any? { return try { diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/MockTest.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/MockTest.kt new file mode 100644 index 000000000..cd620c444 --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/MockTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 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.rule.annotation + +import me.proton.core.test.rule.MockTestRule + +/** + * Test function or test class annotated with this annotation will be treated as mock test by + * [MockTestRule]. + * + * @property scenario - path to the scenario file in tests assets. + * @property isReference - if "true" indicates that this test should be used in recording mode and + * is the reference for recording for a given scenario. + */ +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +public annotation class MockTest( + val scenario: String, + val isReference: Boolean = false +) \ No newline at end of file