From d64107d482b4f3283fb2661a4062050f405bbf43 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 6 Mar 2023 09:47:03 +0100 Subject: [PATCH 01/68] Create a new module for the Api Faker --- gradle/libs.versions.toml | 4 ++ libs/apifaker/build.gradle | 64 ++++++++++++++++++++++ libs/apifaker/src/main/AndroidManifest.xml | 4 ++ settings.gradle | 1 + 4 files changed, 73 insertions(+) create mode 100644 libs/apifaker/build.gradle create mode 100644 libs/apifaker/src/main/AndroidManifest.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe72d2b27f46..24214f404fa2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ androidx-navigation = '2.7.7' androidx-preference = '1.2.1' androidx-recyclerview-main = '1.3.2' androidx-recyclerview-selection = '1.1.0' +androidx-room = "2.6.1" androidx-test-espresso = '3.4.0' androidx-test-ext = '1.1.5' androidx-test-main = '1.4.0' @@ -142,6 +143,9 @@ androidx-preference-main = { group = "androidx.preference", name = "preference", androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "androidx-preference" } androidx-recyclerview-main = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "androidx-recyclerview-main" } androidx-recyclerview-selection = { group = "androidx.recyclerview", name = "recyclerview-selection", version.ref = "androidx-recyclerview-selection" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } androidx-test-espresso-contrib = { group = "androidx.test.espresso", name = "espresso-contrib", version.ref = "androidx-test-espresso" } androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-test-espresso" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext" } diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle new file mode 100644 index 000000000000..2c11fd9b4567 --- /dev/null +++ b/libs/apifaker/build.gradle @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) +} + +repositories { + maven { + url 'https://a8c-libs.s3.amazonaws.com/android' + content { + includeGroup "org.wordpress" + includeGroup "org.wordpress.fluxc" + includeGroup "org.wordpress.fluxc.plugins" + includeGroup "org.wordpress.wellsql" + includeGroup "org.wordpress.mediapicker" + includeGroup "com.automattic" + includeGroup "com.automattic.tracks" + } + } + + maven { + url "https://a8c-libs.s3.amazonaws.com/android/jcenter-mirror" + content { + includeVersion "com.android.volley", "volley", "1.1.1" + } + } +} + +android { + namespace 'com.woocommerce.android.apifaker' + + defaultConfig { + minSdkVersion gradle.ext.minSdkVersion + targetSdk gradle.ext.targetSdkVersion + compileSdk gradle.ext.compileSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose true + } + + compileOptions { + sourceCompatibility libs.versions.java.get() + targetCompatibility libs.versions.java.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.material.main) + implementation(libs.androidx.compose.runtime.livedata) + + implementation("${gradle.ext.fluxCBinaryPath}:${libs.versions.wordpress.fluxc.get()}") { + exclude group: "com.android.support" + exclude group: "org.wordpress", module: "utils" + } + + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) +} diff --git a/libs/apifaker/src/main/AndroidManifest.xml b/libs/apifaker/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..8bdb7e14b389 --- /dev/null +++ b/libs/apifaker/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/settings.gradle b/settings.gradle index 794b7eb05e33..6506be3dedd8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,6 +26,7 @@ include ':quicklogin' include ':libs:commons' include ':libs:cardreader' include ':libs:iap' +include ':libs:apifaker' include ':WooCommerce' gradle.ext.fluxCBinaryPath = "org.wordpress:fluxc" From 61c9e55041be3c18478c77fc2e5275d2e491eab3 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 6 Mar 2023 16:32:20 +0100 Subject: [PATCH 02/68] Add Room DB for holding the endpoints --- .../android/apifaker/db/ApiFakerDatabase.kt | 26 +++++++++++++++++++ .../android/apifaker/db/EndpointDao.kt | 25 ++++++++++++++++++ .../android/apifaker/models/Endpoint.kt | 12 +++++++++ .../android/apifaker/models/EndpointType.kt | 5 ++++ .../apifaker/models/EndpointWithResponse.kt | 13 ++++++++++ .../android/apifaker/models/FakeResponse.kt | 13 ++++++++++ 6 files changed, 94 insertions(+) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointWithResponse.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt new file mode 100644 index 000000000000..e2aa6a27850e --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt @@ -0,0 +1,26 @@ +package com.woocommerce.android.apifaker.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.woocommerce.android.apifaker.models.Endpoint +import com.woocommerce.android.apifaker.models.FakeResponse + +@Database( + entities = [ + Endpoint::class, FakeResponse::class + ], + version = 1 +) +internal abstract class ApiFakerDatabase : RoomDatabase() { + companion object { + fun buildDb(applicationContext: Context) = Room.databaseBuilder( + applicationContext, + ApiFakerDatabase::class.java, + "api-faker-db" + ).build() + } + + abstract val endpointDao: EndpointDao +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt new file mode 100644 index 000000000000..af3090b1e3d0 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -0,0 +1,25 @@ +package com.woocommerce.android.apifaker.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.woocommerce.android.apifaker.models.Endpoint +import com.woocommerce.android.apifaker.models.EndpointWithResponse +import com.woocommerce.android.apifaker.models.FakeResponse +import kotlinx.coroutines.flow.Flow + +@Dao +internal interface EndpointDao { + @Transaction + @Query("Select * FROM Endpoint") + fun observeEndpoints(): Flow> + + @Transaction + @Query("Select * FROM Endpoint WHERE :path LIKE path AND :body LIKE COALESCE(body, '%')") + fun queryEndpoint(path: String, body: String): EndpointWithResponse? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertEndpoint(endpoint: Endpoint, response: FakeResponse) +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt new file mode 100644 index 000000000000..b290010111b1 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt @@ -0,0 +1,12 @@ +package com.woocommerce.android.apifaker.models + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +internal data class Endpoint( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val type: EndpointType, + val path: String, + val body: String? +) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt new file mode 100644 index 000000000000..83ea5697bcd8 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt @@ -0,0 +1,5 @@ +package com.woocommerce.android.apifaker.models + +internal enum class EndpointType { + WPApi, WPCom +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointWithResponse.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointWithResponse.kt new file mode 100644 index 000000000000..6f353dc8e1c0 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointWithResponse.kt @@ -0,0 +1,13 @@ +package com.woocommerce.android.apifaker.models + +import androidx.room.Embedded +import androidx.room.Relation + +internal data class EndpointWithResponse( + @Embedded val endpoint: Endpoint, + @Relation( + parentColumn = "id", + entityColumn = "endpointId" + ) + val response: FakeResponse +) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt new file mode 100644 index 000000000000..90cc67b930fa --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt @@ -0,0 +1,13 @@ +package com.woocommerce.android.apifaker.models + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +internal data class FakeResponse( + @PrimaryKey(autoGenerate = true) val id: Int, + val statusCode: Int, + val body: String?, + val endpointId: Int +) + From 571b1b401b49341e3376660159457c011f828862 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 6 Mar 2023 19:16:07 +0100 Subject: [PATCH 03/68] Add initial logic for intercepting and faking responses --- libs/apifaker/build.gradle | 4 +++ .../android/apifaker/ApiFakerInterceptor.kt | 30 +++++++++++++++++ .../android/apifaker/EndpointProcessor.kt | 26 +++++++++++++++ .../android/apifaker/di/ApiFakerModule.kt | 33 +++++++++++++++++++ .../android/apifaker/models/FakeResponse.kt | 2 +- 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle index 2c11fd9b4567..99b81c112ac4 100644 --- a/libs/apifaker/build.gradle +++ b/libs/apifaker/build.gradle @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.google.dagger.hilt) alias(libs.plugins.ksp) } @@ -61,4 +62,7 @@ dependencies { implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) + + implementation(libs.google.dagger.hilt.android.main) + ksp(libs.google.dagger.hilt.compiler) } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt new file mode 100644 index 000000000000..5ccdb41c130b --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt @@ -0,0 +1,30 @@ +package com.woocommerce.android.apifaker + +import okhttp3.Interceptor +import okhttp3.Interceptor.Chain +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import javax.inject.Inject + +internal class ApiFakerInterceptor @Inject constructor(private val endpointProcessor: EndpointProcessor) : Interceptor { + override fun intercept(chain: Chain): Response { + val request = chain.request() + val fakeResponse = endpointProcessor.fakeRequestIfNeeded(request) + + return if (fakeResponse != null) { + Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .message("Fake Response") + .code(fakeResponse.statusCode) + // TODO check if it's safe to always use JSON as the content type + .body(fakeResponse.body?.toResponseBody("application/json".toMediaType())) + .addHeader("content-type", "application/json") + .build() + } else { + chain.proceed(request) + } + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt new file mode 100644 index 000000000000..eafb110d1b48 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -0,0 +1,26 @@ +package com.woocommerce.android.apifaker + +import com.woocommerce.android.apifaker.db.EndpointDao +import com.woocommerce.android.apifaker.models.FakeResponse +import okhttp3.Request +import okio.Buffer +import javax.inject.Inject + +internal class EndpointProcessor @Inject constructor( + private val endpointDao: EndpointDao +) { + fun fakeRequestIfNeeded(request: Request): FakeResponse? { + // TODO match against WPApi/WPCom, and extract path accordingly + val path = request.url.encodedPath + val requestBody = request.body + val bodyContent = if (requestBody != null) { + val buffer = Buffer() + requestBody.writeTo(buffer) + + buffer.readUtf8() + } else "" + + + return endpointDao.queryEndpoint(path, bodyContent)?.response + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt new file mode 100644 index 000000000000..7b46dfd19f7c --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt @@ -0,0 +1,33 @@ +package com.woocommerce.android.apifaker.di + +import android.content.Context +import com.woocommerce.android.apifaker.ApiFakerInterceptor +import com.woocommerce.android.apifaker.EndpointProcessor +import com.woocommerce.android.apifaker.db.ApiFakerDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import okhttp3.Interceptor +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class ApiFakerModule { + companion object { + @Provides + @Singleton + internal fun providesDatabase(context: Context) = ApiFakerDatabase.buildDb(context) + + @Provides + internal fun providesEndpointDao(db: ApiFakerDatabase) = db.endpointDao + + @Provides + @IntoSet + @Named("interceptors") + internal fun providesInterceptor(endpointProcessor: EndpointProcessor): Interceptor = + ApiFakerInterceptor(endpointProcessor) + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt index 90cc67b930fa..1170055bfe23 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt @@ -5,7 +5,7 @@ import androidx.room.PrimaryKey @Entity internal data class FakeResponse( - @PrimaryKey(autoGenerate = true) val id: Int, + @PrimaryKey(autoGenerate = true) val id: Int = 0, val statusCode: Int, val body: String?, val endpointId: Int From bea598ff1e4d2d785e59438ba5a61e1a3e6c9473 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 7 Mar 2023 11:15:55 +0100 Subject: [PATCH 04/68] Some changes to the DB schema --- .../apifaker/db/EndpointTypeConverter.kt | 24 +++++++++++++++++++ .../android/apifaker/models/Endpoint.kt | 4 +++- .../android/apifaker/models/EndpointType.kt | 7 ++++-- .../android/apifaker/models/FakeResponse.kt | 14 ++++++++--- 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt new file mode 100644 index 000000000000..084c97fa4b4f --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt @@ -0,0 +1,24 @@ +package com.woocommerce.android.apifaker.db + +import androidx.room.TypeConverter +import com.woocommerce.android.apifaker.models.EndpointType + +internal class EndpointTypeConverter { + @TypeConverter + fun fromEndpointType(endpointType: EndpointType?): String? { + if (endpointType == null) return null + return endpointType::class.simpleName + + if (endpointType is EndpointType.Custom) ":${endpointType.host}" else "" + } + + @TypeConverter + fun toEndpointType(value: String?): EndpointType? { + if (value == null) return null + val parts = value.split(":") + return when (parts[0]) { + EndpointType.WPApi::class.simpleName -> EndpointType.WPApi + EndpointType.WPCom::class.simpleName -> EndpointType.WPCom + else -> EndpointType.Custom(parts[1]) + } + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt index b290010111b1..c59706461295 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt @@ -2,11 +2,13 @@ package com.woocommerce.android.apifaker.models import androidx.room.Entity import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.woocommerce.android.apifaker.db.EndpointTypeConverter @Entity internal data class Endpoint( @PrimaryKey(autoGenerate = true) val id: Int = 0, - val type: EndpointType, + @field:TypeConverters(EndpointTypeConverter::class) val type: EndpointType, val path: String, val body: String? ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt index 83ea5697bcd8..fa7772b87057 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt @@ -1,5 +1,8 @@ package com.woocommerce.android.apifaker.models -internal enum class EndpointType { - WPApi, WPCom +internal sealed interface EndpointType { + object WPApi : EndpointType + object WPCom : EndpointType + + data class Custom(val host: String) : EndpointType } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt index 1170055bfe23..ab46710c7c18 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt @@ -1,13 +1,21 @@ package com.woocommerce.android.apifaker.models import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.PrimaryKey -@Entity +@Entity( + foreignKeys = [ + ForeignKey( + entity = Endpoint::class, + parentColumns = ["id"], + childColumns = ["endpointId"] + ) + ] +) internal data class FakeResponse( - @PrimaryKey(autoGenerate = true) val id: Int = 0, + @PrimaryKey val endpointId: Int, val statusCode: Int, val body: String?, - val endpointId: Int ) From e3c4cbbfac89d1ebae57e91fe122a055065ced16 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 7 Mar 2023 12:36:46 +0100 Subject: [PATCH 05/68] Handle extracting data from the request for matching --- libs/apifaker/build.gradle | 4 + .../android/apifaker/EndpointProcessor.kt | 82 ++++++++- .../android/apifaker/db/ApiFakerDatabase.kt | 2 + .../android/apifaker/db/EndpointDao.kt | 5 +- .../android/apifaker/models/Endpoint.kt | 2 +- .../apifaker/util/JSONObjectProvider.kt | 8 + .../android/apifaker/EndpointProcessorTest.kt | 164 ++++++++++++++++++ 7 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt create mode 100644 libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle index 99b81c112ac4..1db98b7683f8 100644 --- a/libs/apifaker/build.gradle +++ b/libs/apifaker/build.gradle @@ -65,4 +65,8 @@ dependencies { implementation(libs.google.dagger.hilt.android.main) ksp(libs.google.dagger.hilt.compiler) + + testImplementation(libs.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index eafb110d1b48..64cb8260c1db 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -1,26 +1,92 @@ package com.woocommerce.android.apifaker import com.woocommerce.android.apifaker.db.EndpointDao +import com.woocommerce.android.apifaker.models.EndpointType import com.woocommerce.android.apifaker.models.FakeResponse +import com.woocommerce.android.apifaker.util.JSONObjectProvider import okhttp3.Request import okio.Buffer import javax.inject.Inject +private const val WPCOM_HOST = "public-api.wordpress.com" +private const val JETPACK_TUNNEL_REGEX = "/rest/v1.1/jetpack-blogs/\\d+/rest-api" + internal class EndpointProcessor @Inject constructor( - private val endpointDao: EndpointDao + private val endpointDao: EndpointDao, + private val jsonObjectProvider: JSONObjectProvider ) { fun fakeRequestIfNeeded(request: Request): FakeResponse? { - // TODO match against WPApi/WPCom, and extract path accordingly - val path = request.url.encodedPath - val requestBody = request.body - val bodyContent = if (requestBody != null) { + // TODO match against method and query parameters too + val endpointData = when { + request.url.host == WPCOM_HOST -> request.extractDataFromWPComEndpoint() + request.url.encodedPath.startsWith("/wp-json") -> request.extractDataFromWPApiEndpoint() + else -> request.extractDataFromCustomEndpoint() + } + + return with(endpointData) { + endpointDao.queryEndpoint(endpointType, path, body.orEmpty()) + }?.response + } + + private fun Request.extractDataFromWPComEndpoint(): EndpointData { + val originalBody = readBody() + return if (url.encodedPath.trimEnd('/').matches(Regex(JETPACK_TUNNEL_REGEX))) { + val (path, body) = if (method == "GET") { + Pair( + url.queryParameter("path")!!, + null + ) + } else { + val jsonObject = jsonObjectProvider.parseString(originalBody) + Pair( + jsonObject.getString("path"), + jsonObject.optString("body") + ) + } + + EndpointData( + endpointType = EndpointType.WPApi, + path = path, + body = body + ) + } else { + EndpointData( + endpointType = EndpointType.WPCom, + path = url.encodedPath.substringAfter("/rest"), + body = originalBody + ) + } + } + + private fun Request.extractDataFromWPApiEndpoint(): EndpointData { + return EndpointData( + endpointType = EndpointType.WPApi, + path = url.encodedPath.substringAfter("/wp-json"), + body = readBody() + ) + } + + private fun Request.extractDataFromCustomEndpoint(): EndpointData { + return EndpointData( + endpointType = EndpointType.Custom(host = url.host), + path = url.encodedPath, + body = readBody() + ) + } + + private fun Request.readBody(): String { + val requestBody = body + return if (requestBody != null) { val buffer = Buffer() requestBody.writeTo(buffer) buffer.readUtf8() } else "" - - - return endpointDao.queryEndpoint(path, bodyContent)?.response } + + private data class EndpointData( + val endpointType: EndpointType, + val path: String, + val body: String? + ) } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt index e2aa6a27850e..76b654d9c119 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import com.woocommerce.android.apifaker.models.Endpoint import com.woocommerce.android.apifaker.models.FakeResponse @@ -13,6 +14,7 @@ import com.woocommerce.android.apifaker.models.FakeResponse ], version = 1 ) +@TypeConverters(EndpointTypeConverter::class) internal abstract class ApiFakerDatabase : RoomDatabase() { companion object { fun buildDb(applicationContext: Context) = Room.databaseBuilder( diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index af3090b1e3d0..57a84ba3f7cc 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import com.woocommerce.android.apifaker.models.Endpoint +import com.woocommerce.android.apifaker.models.EndpointType import com.woocommerce.android.apifaker.models.EndpointWithResponse import com.woocommerce.android.apifaker.models.FakeResponse import kotlinx.coroutines.flow.Flow @@ -17,8 +18,8 @@ internal interface EndpointDao { fun observeEndpoints(): Flow> @Transaction - @Query("Select * FROM Endpoint WHERE :path LIKE path AND :body LIKE COALESCE(body, '%')") - fun queryEndpoint(path: String, body: String): EndpointWithResponse? + @Query("Select * FROM Endpoint WHERE type = :type AND :path LIKE path AND :body LIKE COALESCE(body, '%')") + fun queryEndpoint(type: EndpointType, path: String, body: String): EndpointWithResponse? @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertEndpoint(endpoint: Endpoint, response: FakeResponse) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt index c59706461295..4ee60e9e1f35 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt @@ -8,7 +8,7 @@ import com.woocommerce.android.apifaker.db.EndpointTypeConverter @Entity internal data class Endpoint( @PrimaryKey(autoGenerate = true) val id: Int = 0, - @field:TypeConverters(EndpointTypeConverter::class) val type: EndpointType, + val type: EndpointType, val path: String, val body: String? ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt new file mode 100644 index 000000000000..6f3d05e4dba8 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt @@ -0,0 +1,8 @@ +package com.woocommerce.android.apifaker.util + +import org.json.JSONObject +import javax.inject.Inject + +class JSONObjectProvider @Inject constructor() { + fun parseString(content: String): JSONObject = JSONObject(content) +} diff --git a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt new file mode 100644 index 000000000000..b46a5c7a01fd --- /dev/null +++ b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt @@ -0,0 +1,164 @@ +package com.woocommerce.android.apifaker + +import com.woocommerce.android.apifaker.db.EndpointDao +import com.woocommerce.android.apifaker.models.EndpointType +import com.woocommerce.android.apifaker.util.JSONObjectProvider +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class EndpointProcessorTest { + private val endpointDaoMock = mock() + private val jsonObjectProvider = mock() + private val endpointProcessor = EndpointProcessor( + endpointDao = endpointDaoMock, + jsonObjectProvider = jsonObjectProvider + ) + + @Test + fun `when processing a GET WPCom endpoint, then extract data correctly`() { + val request = Request.Builder() + .method("GET", null) + .url("https://public-api.wordpress.com/rest/v1.1/me?param=value") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.WPCom, + path = "/v1.1/me", + body = "" + ) + } + + @Test + fun `when processing a POST WPCom endpoint, then extract data correctly`() { + val body = "Test Body" + val request = Request.Builder() + .method("POST", body.toRequestBody()) + .url("https://public-api.wordpress.com/rest/v1.1/me?param=value") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.WPCom, + path = "/v1.1/me", + body = body + ) + } + + @Test + fun `when processing a GET Jetpack Tunnel endpoint, then extract data correctly`() { + val request = Request.Builder() + .method("GET", null) + .url("https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/?path=/wc/v3/products") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.WPApi, + path = "/wc/v3/products", + body = "" + ) + } + + @Test + fun `when processing a POST Jetpack Tunnel endpoint, then extract data correctly`() { + val body = """ + "path": "/wc/v3/products", + "body": "test body" + """.trimIndent() + + val jsonObject = mock { + on { getString("path") } doReturn "/wc/v3/products" + on { optString("body") } doReturn "test body" + } + whenever(jsonObjectProvider.parseString(body)).thenReturn(jsonObject) + + val request = Request.Builder() + .method("POST", body.toRequestBody()) + .url("https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.WPApi, + path = "/wc/v3/products", + body = "test body" + ) + } + + @Test + fun `when processing a GET WPApi endpoint, then extract data correctly`() { + val request = Request.Builder() + .method("GET", null) + .url("https://test-site.com/wp-json/wc/v3/products?param=value") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.WPApi, + path = "/wc/v3/products", + body = "" + ) + } + + @Test + fun `when processing a POST WPApi endpoint, then extract data correctly`() { + val body = "Test Body" + val request = Request.Builder() + .method("POST", body.toRequestBody()) + .url("https://test-site.com/wp-json/wc/v3/products") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.WPApi, + path = "/wc/v3/products", + body = body + ) + } + + @Test + fun `when processing a GET Custom endpoint, then extract data correctly`() { + val request = Request.Builder() + .method("GET", null) + .url("https://test-site.com/an/endpoint?param=value") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.Custom("test-site.com"), + path = "/an/endpoint", + body = "" + ) + } + + @Test + fun `when processing a POST Custom endpoint, then extract data correctly`() { + val body = "Test Body" + val request = Request.Builder() + .method("POST", body.toRequestBody()) + .url("https://test-site.com/an/endpoint") + .build() + + endpointProcessor.fakeRequestIfNeeded(request) + + verify(endpointDaoMock).queryEndpoint( + type = EndpointType.Custom("test-site.com"), + path = "/an/endpoint", + body = body + ) + } +} From dccd118ef964f49f37bae60b2a8b563a0b1f82a8 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 7 Mar 2023 12:40:36 +0100 Subject: [PATCH 06/68] Add some logging, and handle errors in parsing --- .../android/apifaker/ApiFakerInterceptor.kt | 13 ++++++++++++- .../java/com/woocommerce/android/apifaker/LogTag.kt | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt index 5ccdb41c130b..e559453a7378 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.apifaker +import android.util.Log import okhttp3.Interceptor import okhttp3.Interceptor.Chain import okhttp3.MediaType.Companion.toMediaType @@ -10,10 +11,20 @@ import javax.inject.Inject internal class ApiFakerInterceptor @Inject constructor(private val endpointProcessor: EndpointProcessor) : Interceptor { override fun intercept(chain: Chain): Response { + Log.d(LOG_TAG, "Intercepting request: ${chain.request().url}") val request = chain.request() - val fakeResponse = endpointProcessor.fakeRequestIfNeeded(request) + val fakeResponse = try { + endpointProcessor.fakeRequestIfNeeded(request) + } catch (e: Exception) { + Log.w(LOG_TAG, "Faking request: ${chain.request()} failed, ignoring") + null + } return if (fakeResponse != null) { + Log.d( + LOG_TAG, "Fake request: ${chain.request().url}:\n" + + "$fakeResponse" + ) Response.Builder() .request(request) .protocol(Protocol.HTTP_1_1) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt new file mode 100644 index 000000000000..fe07c5e2b61b --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt @@ -0,0 +1,3 @@ +package com.woocommerce.android.apifaker + +const val LOG_TAG = "WCApiFaker" From cbf04b6b0e8da7b729ef95b5938ff7fef4651767 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 15:59:57 +0100 Subject: [PATCH 07/68] Add some improvements to the DB schema and dao --- .../android/apifaker/db/EndpointDao.kt | 15 ++++++++++++++- .../android/apifaker/models/Endpoint.kt | 4 +--- .../android/apifaker/models/EndpointType.kt | 8 ++++++-- .../android/apifaker/models/FakeResponse.kt | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index 57a84ba3f7cc..3a567ca341c0 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -21,6 +21,19 @@ internal interface EndpointDao { @Query("Select * FROM Endpoint WHERE type = :type AND :path LIKE path AND :body LIKE COALESCE(body, '%')") fun queryEndpoint(type: EndpointType, path: String, body: String): EndpointWithResponse? + @Transaction + @Query("Select * FROM Endpoint WHERE id = :id") + suspend fun getEndpoint(id: Long): EndpointWithResponse? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertEndpoint(endpoint: Endpoint): Long + @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertEndpoint(endpoint: Endpoint, response: FakeResponse) + suspend fun insertResponse(response: FakeResponse) + + @Transaction + suspend fun insertEndpointWithResponse(endpoint: Endpoint, response: FakeResponse) { + val id = insertEndpoint(endpoint) + insertResponse(response.copy(endpointId = id)) + } } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt index 4ee60e9e1f35..c6cb8f495899 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt @@ -2,12 +2,10 @@ package com.woocommerce.android.apifaker.models import androidx.room.Entity import androidx.room.PrimaryKey -import androidx.room.TypeConverters -import com.woocommerce.android.apifaker.db.EndpointTypeConverter @Entity internal data class Endpoint( - @PrimaryKey(autoGenerate = true) val id: Int = 0, + @PrimaryKey(autoGenerate = true) val id: Long = 0, val type: EndpointType, val path: String, val body: String? diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt index fa7772b87057..50f62a1911d9 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt @@ -1,8 +1,12 @@ package com.woocommerce.android.apifaker.models internal sealed interface EndpointType { - object WPApi : EndpointType - object WPCom : EndpointType + companion object { + fun defaultValues(): List = listOf(WPApi, WPCom, Custom("")) + } + + data object WPApi : EndpointType + data object WPCom : EndpointType data class Custom(val host: String) : EndpointType } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt index ab46710c7c18..3ab300c96388 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt @@ -14,7 +14,7 @@ import androidx.room.PrimaryKey ] ) internal data class FakeResponse( - @PrimaryKey val endpointId: Int, + @PrimaryKey val endpointId: Long = 0, val statusCode: Int, val body: String?, ) From a9ad0cac40ba97669570de2306e085f57f38718f Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 3 Dec 2024 12:14:52 +0100 Subject: [PATCH 08/68] Rename classes --- .../android/apifaker/EndpointProcessor.kt | 18 ++++++------ .../android/apifaker/db/ApiFakerDatabase.kt | 6 ++-- .../android/apifaker/db/EndpointDao.kt | 28 +++++++++---------- .../apifaker/db/EndpointTypeConverter.kt | 18 ++++++------ .../android/apifaker/models/ApiType.kt | 12 ++++++++ .../android/apifaker/models/EndpointType.kt | 12 -------- ...pointWithResponse.kt => MockedEndpoint.kt} | 6 ++-- .../models/{Endpoint.kt => Request.kt} | 4 +-- .../models/{FakeResponse.kt => Response.kt} | 4 +-- .../android/apifaker/EndpointProcessorTest.kt | 18 ++++++------ 10 files changed, 63 insertions(+), 63 deletions(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/ApiType.kt delete mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt rename libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/{EndpointWithResponse.kt => MockedEndpoint.kt} (64%) rename libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/{Endpoint.kt => Request.kt} (79%) rename libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/{FakeResponse.kt => Response.kt} (84%) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 64cb8260c1db..8e3f01cced9b 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -1,8 +1,8 @@ package com.woocommerce.android.apifaker import com.woocommerce.android.apifaker.db.EndpointDao -import com.woocommerce.android.apifaker.models.EndpointType -import com.woocommerce.android.apifaker.models.FakeResponse +import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.util.JSONObjectProvider import okhttp3.Request import okio.Buffer @@ -15,7 +15,7 @@ internal class EndpointProcessor @Inject constructor( private val endpointDao: EndpointDao, private val jsonObjectProvider: JSONObjectProvider ) { - fun fakeRequestIfNeeded(request: Request): FakeResponse? { + fun fakeRequestIfNeeded(request: Request): Response? { // TODO match against method and query parameters too val endpointData = when { request.url.host == WPCOM_HOST -> request.extractDataFromWPComEndpoint() @@ -24,7 +24,7 @@ internal class EndpointProcessor @Inject constructor( } return with(endpointData) { - endpointDao.queryEndpoint(endpointType, path, body.orEmpty()) + endpointDao.queryEndpoint(apiType, path, body.orEmpty()) }?.response } @@ -45,13 +45,13 @@ internal class EndpointProcessor @Inject constructor( } EndpointData( - endpointType = EndpointType.WPApi, + apiType = ApiType.WPApi, path = path, body = body ) } else { EndpointData( - endpointType = EndpointType.WPCom, + apiType = ApiType.WPCom, path = url.encodedPath.substringAfter("/rest"), body = originalBody ) @@ -60,7 +60,7 @@ internal class EndpointProcessor @Inject constructor( private fun Request.extractDataFromWPApiEndpoint(): EndpointData { return EndpointData( - endpointType = EndpointType.WPApi, + apiType = ApiType.WPApi, path = url.encodedPath.substringAfter("/wp-json"), body = readBody() ) @@ -68,7 +68,7 @@ internal class EndpointProcessor @Inject constructor( private fun Request.extractDataFromCustomEndpoint(): EndpointData { return EndpointData( - endpointType = EndpointType.Custom(host = url.host), + apiType = ApiType.Custom(host = url.host), path = url.encodedPath, body = readBody() ) @@ -85,7 +85,7 @@ internal class EndpointProcessor @Inject constructor( } private data class EndpointData( - val endpointType: EndpointType, + val apiType: ApiType, val path: String, val body: String? ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt index 76b654d9c119..0ee2dd0a41ea 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt @@ -5,12 +5,12 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import com.woocommerce.android.apifaker.models.Endpoint -import com.woocommerce.android.apifaker.models.FakeResponse +import com.woocommerce.android.apifaker.models.Request +import com.woocommerce.android.apifaker.models.Response @Database( entities = [ - Endpoint::class, FakeResponse::class + Request::class, Response::class ], version = 1 ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index 3a567ca341c0..dc4ab64fbd02 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -5,35 +5,35 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction -import com.woocommerce.android.apifaker.models.Endpoint -import com.woocommerce.android.apifaker.models.EndpointType -import com.woocommerce.android.apifaker.models.EndpointWithResponse -import com.woocommerce.android.apifaker.models.FakeResponse +import com.woocommerce.android.apifaker.models.Request +import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.MockedEndpoint +import com.woocommerce.android.apifaker.models.Response import kotlinx.coroutines.flow.Flow @Dao internal interface EndpointDao { @Transaction - @Query("Select * FROM Endpoint") - fun observeEndpoints(): Flow> + @Query("Select * FROM Request") + fun observeEndpoints(): Flow> @Transaction - @Query("Select * FROM Endpoint WHERE type = :type AND :path LIKE path AND :body LIKE COALESCE(body, '%')") - fun queryEndpoint(type: EndpointType, path: String, body: String): EndpointWithResponse? + @Query("Select * FROM Request WHERE type = :type AND :path LIKE path AND :body LIKE COALESCE(body, '%')") + fun queryEndpoint(type: ApiType, path: String, body: String): MockedEndpoint? @Transaction - @Query("Select * FROM Endpoint WHERE id = :id") - suspend fun getEndpoint(id: Long): EndpointWithResponse? + @Query("Select * FROM Request WHERE id = :id") + suspend fun getEndpoint(id: Long): MockedEndpoint? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertEndpoint(endpoint: Endpoint): Long + suspend fun insertRequest(request: Request): Long @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertResponse(response: FakeResponse) + suspend fun insertResponse(response: Response) @Transaction - suspend fun insertEndpointWithResponse(endpoint: Endpoint, response: FakeResponse) { - val id = insertEndpoint(endpoint) + suspend fun insertEndpoint(request: Request, response: Response) { + val id = insertRequest(request) insertResponse(response.copy(endpointId = id)) } } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt index 084c97fa4b4f..c7b59095718f 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointTypeConverter.kt @@ -1,24 +1,24 @@ package com.woocommerce.android.apifaker.db import androidx.room.TypeConverter -import com.woocommerce.android.apifaker.models.EndpointType +import com.woocommerce.android.apifaker.models.ApiType internal class EndpointTypeConverter { @TypeConverter - fun fromEndpointType(endpointType: EndpointType?): String? { - if (endpointType == null) return null - return endpointType::class.simpleName + - if (endpointType is EndpointType.Custom) ":${endpointType.host}" else "" + fun fromEndpointType(apiType: ApiType?): String? { + if (apiType == null) return null + return apiType::class.simpleName + + if (apiType is ApiType.Custom) ":${apiType.host}" else "" } @TypeConverter - fun toEndpointType(value: String?): EndpointType? { + fun toEndpointType(value: String?): ApiType? { if (value == null) return null val parts = value.split(":") return when (parts[0]) { - EndpointType.WPApi::class.simpleName -> EndpointType.WPApi - EndpointType.WPCom::class.simpleName -> EndpointType.WPCom - else -> EndpointType.Custom(parts[1]) + ApiType.WPApi::class.simpleName -> ApiType.WPApi + ApiType.WPCom::class.simpleName -> ApiType.WPCom + else -> ApiType.Custom(parts[1]) } } } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/ApiType.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/ApiType.kt new file mode 100644 index 000000000000..b00d636763c4 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/ApiType.kt @@ -0,0 +1,12 @@ +package com.woocommerce.android.apifaker.models + +internal sealed interface ApiType { + companion object { + fun defaultValues(): List = listOf(WPApi, WPCom, Custom("")) + } + + data object WPApi : ApiType + data object WPCom : ApiType + + data class Custom(val host: String) : ApiType +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt deleted file mode 100644 index 50f62a1911d9..000000000000 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointType.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.woocommerce.android.apifaker.models - -internal sealed interface EndpointType { - companion object { - fun defaultValues(): List = listOf(WPApi, WPCom, Custom("")) - } - - data object WPApi : EndpointType - data object WPCom : EndpointType - - data class Custom(val host: String) : EndpointType -} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointWithResponse.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/MockedEndpoint.kt similarity index 64% rename from libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointWithResponse.kt rename to libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/MockedEndpoint.kt index 6f353dc8e1c0..b7cf3bbccb0c 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/EndpointWithResponse.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/MockedEndpoint.kt @@ -3,11 +3,11 @@ package com.woocommerce.android.apifaker.models import androidx.room.Embedded import androidx.room.Relation -internal data class EndpointWithResponse( - @Embedded val endpoint: Endpoint, +internal data class MockedEndpoint( + @Embedded val request: Request, @Relation( parentColumn = "id", entityColumn = "endpointId" ) - val response: FakeResponse + val response: Response ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt similarity index 79% rename from libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt rename to libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt index c6cb8f495899..aaa2d77deb90 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Endpoint.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt @@ -4,9 +4,9 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity -internal data class Endpoint( +internal data class Request( @PrimaryKey(autoGenerate = true) val id: Long = 0, - val type: EndpointType, + val type: ApiType, val path: String, val body: String? ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt similarity index 84% rename from libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt rename to libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt index 3ab300c96388..d62cc6c5f18e 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/FakeResponse.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt @@ -7,13 +7,13 @@ import androidx.room.PrimaryKey @Entity( foreignKeys = [ ForeignKey( - entity = Endpoint::class, + entity = Request::class, parentColumns = ["id"], childColumns = ["endpointId"] ) ] ) -internal data class FakeResponse( +internal data class Response( @PrimaryKey val endpointId: Long = 0, val statusCode: Int, val body: String?, diff --git a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt index b46a5c7a01fd..6ff722d3292d 100644 --- a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt +++ b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt @@ -1,7 +1,7 @@ package com.woocommerce.android.apifaker import com.woocommerce.android.apifaker.db.EndpointDao -import com.woocommerce.android.apifaker.models.EndpointType +import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.util.JSONObjectProvider import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody @@ -30,7 +30,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.WPCom, + type = ApiType.WPCom, path = "/v1.1/me", body = "" ) @@ -47,7 +47,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.WPCom, + type = ApiType.WPCom, path = "/v1.1/me", body = body ) @@ -63,7 +63,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.WPApi, + type = ApiType.WPApi, path = "/wc/v3/products", body = "" ) @@ -90,7 +90,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.WPApi, + type = ApiType.WPApi, path = "/wc/v3/products", body = "test body" ) @@ -106,7 +106,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.WPApi, + type = ApiType.WPApi, path = "/wc/v3/products", body = "" ) @@ -123,7 +123,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.WPApi, + type = ApiType.WPApi, path = "/wc/v3/products", body = body ) @@ -139,7 +139,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.Custom("test-site.com"), + type = ApiType.Custom("test-site.com"), path = "/an/endpoint", body = "" ) @@ -156,7 +156,7 @@ class EndpointProcessorTest { endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( - type = EndpointType.Custom("test-site.com"), + type = ApiType.Custom("test-site.com"), path = "/an/endpoint", body = body ) From 9864ff6a5f60733e9f85945e33be31b3159b7723 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 3 Dec 2024 12:49:15 +0100 Subject: [PATCH 09/68] Fix detekt issues --- .../android/apifaker/ApiFakerInterceptor.kt | 6 ++--- .../android/apifaker/EndpointProcessor.kt | 4 +++- .../android/apifaker/db/EndpointDao.kt | 2 +- .../android/apifaker/di/ApiFakerModule.kt | 24 +++++++++---------- .../android/apifaker/models/Response.kt | 1 - 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt index e559453a7378..6a3c8602bcaf 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt @@ -10,6 +10,7 @@ import okhttp3.ResponseBody.Companion.toResponseBody import javax.inject.Inject internal class ApiFakerInterceptor @Inject constructor(private val endpointProcessor: EndpointProcessor) : Interceptor { + @Suppress("TooGenericExceptionCaught", "SwallowedException") override fun intercept(chain: Chain): Response { Log.d(LOG_TAG, "Intercepting request: ${chain.request().url}") val request = chain.request() @@ -21,10 +22,7 @@ internal class ApiFakerInterceptor @Inject constructor(private val endpointProce } return if (fakeResponse != null) { - Log.d( - LOG_TAG, "Fake request: ${chain.request().url}:\n" + - "$fakeResponse" - ) + Log.d(LOG_TAG, "Fake request: ${chain.request().url}:\n$fakeResponse") Response.Builder() .request(request) .protocol(Protocol.HTTP_1_1) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 8e3f01cced9b..18520f45755d 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -81,7 +81,9 @@ internal class EndpointProcessor @Inject constructor( requestBody.writeTo(buffer) buffer.readUtf8() - } else "" + } else { + "" + } } private data class EndpointData( diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index dc4ab64fbd02..a5d7c5eab0bc 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -5,9 +5,9 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction -import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.MockedEndpoint +import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import kotlinx.coroutines.flow.Flow diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt index 7b46dfd19f7c..da3b649e3fea 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt @@ -15,19 +15,17 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -abstract class ApiFakerModule { - companion object { - @Provides - @Singleton - internal fun providesDatabase(context: Context) = ApiFakerDatabase.buildDb(context) +object ApiFakerModule { + @Provides + @Singleton + internal fun providesDatabase(context: Context) = ApiFakerDatabase.buildDb(context) - @Provides - internal fun providesEndpointDao(db: ApiFakerDatabase) = db.endpointDao + @Provides + internal fun providesEndpointDao(db: ApiFakerDatabase) = db.endpointDao - @Provides - @IntoSet - @Named("interceptors") - internal fun providesInterceptor(endpointProcessor: EndpointProcessor): Interceptor = - ApiFakerInterceptor(endpointProcessor) - } + @Provides + @IntoSet + @Named("interceptors") + internal fun providesInterceptor(endpointProcessor: EndpointProcessor): Interceptor = + ApiFakerInterceptor(endpointProcessor) } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt index d62cc6c5f18e..d04db05ffb65 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt @@ -18,4 +18,3 @@ internal data class Response( val statusCode: Int, val body: String?, ) - From 758606435bdb7287b8c680a46a005ab15f723823 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 3 Dec 2024 15:10:17 +0100 Subject: [PATCH 10/68] Run unit tests of all libraries in CI --- .buildkite/commands/run-unit-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/commands/run-unit-tests.sh b/.buildkite/commands/run-unit-tests.sh index 7419ce409912..9d28c534e675 100755 --- a/.buildkite/commands/run-unit-tests.sh +++ b/.buildkite/commands/run-unit-tests.sh @@ -2,7 +2,7 @@ echo "--- 🧪 Testing" set +e -./gradlew testJalapenoDebugUnitTest lib:cardreader:testDebugUnitTest lib:iap:testDebugUnitTest +./gradlew testJalapenoDebugUnitTest testDebugUnitTest TESTS_EXIT_STATUS=$? set -e From 97f8325713d544f951b7e640207bd44e09492d68 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 10 Mar 2023 11:07:31 +0100 Subject: [PATCH 11/68] Disable schema export --- .../com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt index 0ee2dd0a41ea..bead0d0d2b04 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt @@ -12,7 +12,8 @@ import com.woocommerce.android.apifaker.models.Response entities = [ Request::class, Response::class ], - version = 1 + version = 1, + exportSchema = false ) @TypeConverters(EndpointTypeConverter::class) internal abstract class ApiFakerDatabase : RoomDatabase() { From 566bb53050f1357fd7c6b975384f377dc95a49e7 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 09:26:45 +0100 Subject: [PATCH 12/68] Enable destructive migration --- .../android/apifaker/db/ApiFakerDatabase.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt index bead0d0d2b04..d95829ab6353 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt @@ -10,7 +10,8 @@ import com.woocommerce.android.apifaker.models.Response @Database( entities = [ - Request::class, Response::class + Request::class, + Response::class ], version = 1, exportSchema = false @@ -18,11 +19,14 @@ import com.woocommerce.android.apifaker.models.Response @TypeConverters(EndpointTypeConverter::class) internal abstract class ApiFakerDatabase : RoomDatabase() { companion object { - fun buildDb(applicationContext: Context) = Room.databaseBuilder( - applicationContext, - ApiFakerDatabase::class.java, - "api-faker-db" - ).build() + fun buildDb(applicationContext: Context) = Room + .databaseBuilder( + context = applicationContext, + klass = ApiFakerDatabase::class.java, + name = "api-faker-db" + ) + .fallbackToDestructiveMigration() + .build() } abstract val endpointDao: EndpointDao From cd0488232fb39a5462c4cff329e06c2c1a0504ad Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 16:48:29 +0100 Subject: [PATCH 13/68] Improve the query statement --- .../java/com/woocommerce/android/apifaker/db/EndpointDao.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index a5d7c5eab0bc..726dccf1378f 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -18,7 +18,11 @@ internal interface EndpointDao { fun observeEndpoints(): Flow> @Transaction - @Query("Select * FROM Request WHERE type = :type AND :path LIKE path AND :body LIKE COALESCE(body, '%')") + @Query("""Select * FROM Request WHERE + type = :type AND + :path LIKE path AND + :body LIKE COALESCE(body, '%') + """) fun queryEndpoint(type: ApiType, path: String, body: String): MockedEndpoint? @Transaction From e637209a80c123ab0075f478a2574db291c48e57 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 18:24:39 +0100 Subject: [PATCH 14/68] Add ability to delete requests --- .../com/woocommerce/android/apifaker/db/EndpointDao.kt | 9 +++++++++ .../com/woocommerce/android/apifaker/models/Response.kt | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index 726dccf1378f..c3d7625b58c9 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.apifaker.db import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -17,6 +18,9 @@ internal interface EndpointDao { @Query("Select * FROM Request") fun observeEndpoints(): Flow> + @Query("Select COUNT(*) FROM Request") + suspend fun endpointsCount(): Int + @Transaction @Query("""Select * FROM Request WHERE type = :type AND @@ -35,9 +39,14 @@ internal interface EndpointDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertResponse(response: Response) + @Delete + suspend fun deleteRequest(request: Request) + @Transaction suspend fun insertEndpoint(request: Request, response: Response) { val id = insertRequest(request) insertResponse(response.copy(endpointId = id)) } + + suspend fun isEmpty() = endpointsCount() == 0 } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt index d04db05ffb65..02b3d74d41b5 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt @@ -9,7 +9,8 @@ import androidx.room.PrimaryKey ForeignKey( entity = Request::class, parentColumns = ["id"], - childColumns = ["endpointId"] + childColumns = ["endpointId"], + onDelete = ForeignKey.CASCADE ) ] ) From 1eb669e054e3160379a3f69ac2ad9e1361b7fa89 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 09:26:45 +0100 Subject: [PATCH 15/68] Add HttpMethod to the DB entities --- .../woocommerce/android/apifaker/EndpointProcessor.kt | 6 +++++- .../com/woocommerce/android/apifaker/db/EndpointDao.kt | 10 +++++++--- .../woocommerce/android/apifaker/models/HttpMethod.kt | 5 +++++ .../com/woocommerce/android/apifaker/models/Request.kt | 1 + 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 18520f45755d..4de239e4aa59 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -2,6 +2,7 @@ package com.woocommerce.android.apifaker import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.util.JSONObjectProvider import okhttp3.Request @@ -24,7 +25,7 @@ internal class EndpointProcessor @Inject constructor( } return with(endpointData) { - endpointDao.queryEndpoint(apiType, path, body.orEmpty()) + endpointDao.queryEndpoint(apiType, request.httpMethod, path.trimEnd('/'), body.orEmpty()) }?.response } @@ -86,6 +87,9 @@ internal class EndpointProcessor @Inject constructor( } } + private val Request.httpMethod + get() = HttpMethod.valueOf(this.method.uppercase()) + private data class EndpointData( val apiType: ApiType, val path: String, diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index c3d7625b58c9..1c1c47d0c993 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -7,6 +7,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.models.MockedEndpoint import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response @@ -22,12 +23,15 @@ internal interface EndpointDao { suspend fun endpointsCount(): Int @Transaction - @Query("""Select * FROM Request WHERE + @Query( + """Select * FROM Request WHERE type = :type AND + (httpMethod is NULL OR httpMethod = :httpMethod) AND :path LIKE path AND :body LIKE COALESCE(body, '%') - """) - fun queryEndpoint(type: ApiType, path: String, body: String): MockedEndpoint? + """ + ) + fun queryEndpoint(type: ApiType, httpMethod: HttpMethod, path: String, body: String): MockedEndpoint? @Transaction @Query("Select * FROM Request WHERE id = :id") diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt new file mode 100644 index 000000000000..722ffac7c18b --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt @@ -0,0 +1,5 @@ +package com.woocommerce.android.apifaker.models + +enum class HttpMethod { + GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, TRACE, CONNECT +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt index aaa2d77deb90..2f606730775c 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt @@ -7,6 +7,7 @@ import androidx.room.PrimaryKey internal data class Request( @PrimaryKey(autoGenerate = true) val id: Long = 0, val type: ApiType, + val httpMethod: HttpMethod?, val path: String, val body: String? ) From 9a5c208d73143a2be3ff37cffa16ad2b671ac711 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 16:28:24 +0100 Subject: [PATCH 16/68] Fix endpoint extraction from Jetpack tunnel endpoints --- .../woocommerce/android/apifaker/EndpointProcessor.kt | 4 ++-- .../android/apifaker/EndpointProcessorTest.kt | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 4de239e4aa59..e7a254650262 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -34,13 +34,13 @@ internal class EndpointProcessor @Inject constructor( return if (url.encodedPath.trimEnd('/').matches(Regex(JETPACK_TUNNEL_REGEX))) { val (path, body) = if (method == "GET") { Pair( - url.queryParameter("path")!!, + url.queryParameter("path")!!.substringBefore("&"), null ) } else { val jsonObject = jsonObjectProvider.parseString(originalBody) Pair( - jsonObject.getString("path"), + jsonObject.getString("path").substringBefore("&"), jsonObject.optString("body") ) } diff --git a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt index 6ff722d3292d..52d6c0081216 100644 --- a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt +++ b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt @@ -2,6 +2,7 @@ package com.woocommerce.android.apifaker import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.util.JSONObjectProvider import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody @@ -31,6 +32,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.WPCom, + httpMethod = HttpMethod.GET, path = "/v1.1/me", body = "" ) @@ -48,6 +50,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.WPCom, + httpMethod = HttpMethod.POST, path = "/v1.1/me", body = body ) @@ -57,13 +60,14 @@ class EndpointProcessorTest { fun `when processing a GET Jetpack Tunnel endpoint, then extract data correctly`() { val request = Request.Builder() .method("GET", null) - .url("https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/?path=/wc/v3/products") + .url("https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/?path=/wc/v3/products&_method=get") .build() endpointProcessor.fakeRequestIfNeeded(request) verify(endpointDaoMock).queryEndpoint( type = ApiType.WPApi, + httpMethod = HttpMethod.GET, path = "/wc/v3/products", body = "" ) @@ -91,6 +95,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.WPApi, + httpMethod = HttpMethod.POST, path = "/wc/v3/products", body = "test body" ) @@ -107,6 +112,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.WPApi, + httpMethod = HttpMethod.GET, path = "/wc/v3/products", body = "" ) @@ -124,6 +130,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.WPApi, + httpMethod = HttpMethod.POST, path = "/wc/v3/products", body = body ) @@ -140,6 +147,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.Custom("test-site.com"), + httpMethod = HttpMethod.GET, path = "/an/endpoint", body = "" ) @@ -157,6 +165,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.Custom("test-site.com"), + httpMethod = HttpMethod.POST, path = "/an/endpoint", body = body ) From 124ecf7068ed935ce31ea3d7c7f2d1d5b2a15b9c Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 15:01:26 +0100 Subject: [PATCH 17/68] Fix detekt issue --- .../woocommerce/android/apifaker/EndpointProcessorTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt index 52d6c0081216..6f057f224491 100644 --- a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt +++ b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt @@ -60,7 +60,10 @@ class EndpointProcessorTest { fun `when processing a GET Jetpack Tunnel endpoint, then extract data correctly`() { val request = Request.Builder() .method("GET", null) - .url("https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/?path=/wc/v3/products&_method=get") + .url( + "https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/" + + "?path=/wc/v3/products&_method=get" + ) .build() endpointProcessor.fakeRequestIfNeeded(request) From 4982268b12b7ac116ffff3077920ac6f8077ad7c Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 18:38:24 +0100 Subject: [PATCH 18/68] Improve logic of handling Jetpack tunnel endpoints This commit adds logic for extracting the method from the path, and also the logic to wrap the body using `data` property when needed --- .../android/apifaker/EndpointProcessor.kt | 34 ++++++++++++--- .../android/apifaker/EndpointProcessorTest.kt | 43 ++++++++++++++++++- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index e7a254650262..6716987bec30 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -5,6 +5,7 @@ import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.util.JSONObjectProvider +import okhttp3.HttpUrl import okhttp3.Request import okio.Buffer import javax.inject.Inject @@ -25,34 +26,41 @@ internal class EndpointProcessor @Inject constructor( } return with(endpointData) { - endpointDao.queryEndpoint(apiType, request.httpMethod, path.trimEnd('/'), body.orEmpty()) - }?.response + endpointDao.queryEndpoint(apiType, endpointData.httpMethod, path.trimEnd('/'), body.orEmpty()) + }?.response?.let { + it.copy(body = it.body?.wrapBodyIfNecessary(request.url)) + } } private fun Request.extractDataFromWPComEndpoint(): EndpointData { val originalBody = readBody() return if (url.encodedPath.trimEnd('/').matches(Regex(JETPACK_TUNNEL_REGEX))) { - val (path, body) = if (method == "GET") { - Pair( + val (path, method, body) = if (method == "GET") { + Triple( url.queryParameter("path")!!.substringBefore("&"), + url.queryParameter("_method") ?: "GET", null ) } else { val jsonObject = jsonObjectProvider.parseString(originalBody) - Pair( - jsonObject.getString("path").substringBefore("&"), + val pathParts = jsonObject.getString("path").split("&") + Triple( + pathParts[0], + pathParts.getOrNull(1)?.split("=")?.getOrNull(1) ?: "POST", jsonObject.optString("body") ) } EndpointData( apiType = ApiType.WPApi, + httpMethod = HttpMethod.valueOf(method.uppercase()), path = path, body = body ) } else { EndpointData( apiType = ApiType.WPCom, + httpMethod = httpMethod, path = url.encodedPath.substringAfter("/rest"), body = originalBody ) @@ -62,6 +70,7 @@ internal class EndpointProcessor @Inject constructor( private fun Request.extractDataFromWPApiEndpoint(): EndpointData { return EndpointData( apiType = ApiType.WPApi, + httpMethod = httpMethod, path = url.encodedPath.substringAfter("/wp-json"), body = readBody() ) @@ -70,6 +79,7 @@ internal class EndpointProcessor @Inject constructor( private fun Request.extractDataFromCustomEndpoint(): EndpointData { return EndpointData( apiType = ApiType.Custom(host = url.host), + httpMethod = httpMethod, path = url.encodedPath, body = readBody() ) @@ -87,12 +97,24 @@ internal class EndpointProcessor @Inject constructor( } } + private fun String.wrapBodyIfNecessary(url: HttpUrl): String { + return if (url.host == WPCOM_HOST && + url.encodedPath.trimEnd('/').matches(Regex(JETPACK_TUNNEL_REGEX)) && + !startsWith("{\"data\":") + ) { + "{\"data\": $this}" + } else { + this + } + } + private val Request.httpMethod get() = HttpMethod.valueOf(this.method.uppercase()) private data class EndpointData( val apiType: ApiType, val path: String, + val httpMethod: HttpMethod, val body: String? ) } diff --git a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt index 6f057f224491..7cbfcd77e924 100644 --- a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt +++ b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt @@ -3,6 +3,8 @@ package com.woocommerce.android.apifaker import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod +import com.woocommerce.android.apifaker.models.MockedEndpoint +import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.util.JSONObjectProvider import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody @@ -79,12 +81,12 @@ class EndpointProcessorTest { @Test fun `when processing a POST Jetpack Tunnel endpoint, then extract data correctly`() { val body = """ - "path": "/wc/v3/products", + "path": "/wc/v3/products&_method=post", "body": "test body" """.trimIndent() val jsonObject = mock { - on { getString("path") } doReturn "/wc/v3/products" + on { getString("path") } doReturn "/wc/v3/products&_method=post" on { optString("body") } doReturn "test body" } whenever(jsonObjectProvider.parseString(body)).thenReturn(jsonObject) @@ -173,4 +175,41 @@ class EndpointProcessorTest { body = body ) } + + @Test + fun `when processing a jetpack tunnel endpoint, then wrap body if necessary`() { + val mockEndpoint = MockedEndpoint( + request = com.woocommerce.android.apifaker.models.Request( + id = 0, + type = ApiType.WPApi, + httpMethod = HttpMethod.GET, + path = "/wc/v3/products", + body = null + ), + response = Response( + endpointId = 0, + statusCode = 200, + body = "{\"key\":\"value\"}" + ) + ) + whenever( + endpointDaoMock.queryEndpoint( + type = mockEndpoint.request.type, + httpMethod = mockEndpoint.request.httpMethod!!, + path = mockEndpoint.request.path, + body = mockEndpoint.request.body.orEmpty() + ) + ).thenReturn(mockEndpoint) + + val request = Request.Builder() + .method(mockEndpoint.request.httpMethod.name, null) + .url( + "https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/" + + "?path=${mockEndpoint.request.path}&_method=${mockEndpoint.request.httpMethod.name}" + ) + .build() + + val response = endpointProcessor.fakeRequestIfNeeded(request) + assert(response?.body == "{\"data\": {\"key\":\"value\"}}") + } } From 73ba1620b8e0cb32614df0a58cc6c3c90fd159ee Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 5 Dec 2024 10:25:51 +0100 Subject: [PATCH 19/68] Improve the logic of extracting the http method for Jetpack tunneled requests --- .../com/woocommerce/android/apifaker/EndpointProcessor.kt | 6 ++++-- .../woocommerce/android/apifaker/EndpointProcessorTest.kt | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 6716987bec30..254c3d5ebbe1 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -44,9 +44,11 @@ internal class EndpointProcessor @Inject constructor( } else { val jsonObject = jsonObjectProvider.parseString(originalBody) val pathParts = jsonObject.getString("path").split("&") + val path = pathParts[0] + val method = pathParts.firstOrNull { it.startsWith("_method") }?.split("=")?.getOrNull(1) ?: "POST" Triple( - pathParts[0], - pathParts.getOrNull(1)?.split("=")?.getOrNull(1) ?: "POST", + path, + method, jsonObject.optString("body") ) } diff --git a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt index 7cbfcd77e924..ae84ddc641b7 100644 --- a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt +++ b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt @@ -81,12 +81,12 @@ class EndpointProcessorTest { @Test fun `when processing a POST Jetpack Tunnel endpoint, then extract data correctly`() { val body = """ - "path": "/wc/v3/products&_method=post", + "path": "/wc/v3/products&_method=put", "body": "test body" """.trimIndent() val jsonObject = mock { - on { getString("path") } doReturn "/wc/v3/products&_method=post" + on { getString("path") } doReturn "/wc/v3/products&_method=put" on { optString("body") } doReturn "test body" } whenever(jsonObjectProvider.parseString(body)).thenReturn(jsonObject) @@ -100,7 +100,7 @@ class EndpointProcessorTest { verify(endpointDaoMock).queryEndpoint( type = ApiType.WPApi, - httpMethod = HttpMethod.POST, + httpMethod = HttpMethod.PUT, path = "/wc/v3/products", body = "test body" ) @@ -177,7 +177,7 @@ class EndpointProcessorTest { } @Test - fun `when processing a jetpack tunnel endpoint, then wrap body if necessary`() { + fun `when processing a GET jetpack tunnel endpoint, then wrap body if necessary`() { val mockEndpoint = MockedEndpoint( request = com.woocommerce.android.apifaker.models.Request( id = 0, From 3d1c8c94af74d19ff09ca57a316295e329484778 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 5 Dec 2024 10:32:07 +0100 Subject: [PATCH 20/68] Mark ApiFaker log tag as internal --- .../src/main/java/com/woocommerce/android/apifaker/LogTag.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt index fe07c5e2b61b..3bc6836eb1d5 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/LogTag.kt @@ -1,3 +1,3 @@ package com.woocommerce.android.apifaker -const val LOG_TAG = "WCApiFaker" +internal const val LOG_TAG = "WCApiFaker" From c8ca8a3734bbc8da9bb423c0880d59dce38cc71e Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 18:07:55 +0100 Subject: [PATCH 21/68] Add an initial screen to list the different mocked endpoints --- libs/apifaker/build.gradle | 5 + .../apifaker/ui/ApiFakerHostFragment.kt | 48 ++++++ .../woocommerce/android/apifaker/ui/Screen.kt | 13 ++ .../android/apifaker/ui/home/HomeScreen.kt | 151 ++++++++++++++++++ .../android/apifaker/ui/home/HomeViewModel.kt | 23 +++ 5 files changed, 240 insertions(+) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle index 1db98b7683f8..6da1f26b093f 100644 --- a/libs/apifaker/build.gradle +++ b/libs/apifaker/build.gradle @@ -50,9 +50,14 @@ android { } dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.navigation.compose) + implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.material.main) implementation(libs.androidx.compose.runtime.livedata) + implementation(libs.androidx.compose.ui.tooling.preview) + debugImplementation(libs.androidx.compose.ui.tooling.main) implementation("${gradle.ext.fluxCBinaryPath}:${libs.versions.wordpress.fluxc.get()}") { exclude group: "com.android.support" diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt new file mode 100644 index 000000000000..eafbec1ad751 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt @@ -0,0 +1,48 @@ +package com.woocommerce.android.apifaker.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.woocommerce.android.apifaker.ui.home.HomeScreen +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ApiFakerHostFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + + setContent { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = Screen.Home.route() + ) { + composable(Screen.Home.route()) { + HomeScreen(navController) + } + composable( + Screen.EndpointDetails.baseRoute, + arguments = listOf(navArgument("endpointId") { + type = NavType.IntType + defaultValue = -1 + }) + ) { + TODO() + } + } + } + } + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt new file mode 100644 index 000000000000..bbcf0bf0a301 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt @@ -0,0 +1,13 @@ +package com.woocommerce.android.apifaker.ui + +internal sealed class Screen(val baseRoute: String) { + object Home : Screen("home") { + fun route() = baseRoute + } + + object EndpointDetails : Screen("/endpoint-details") { + fun route(endpointId: Long) = "$baseRoute/$endpointId" + + fun routeForCreation() = baseRoute + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt new file mode 100644 index 000000000000..fc713b83b6ff --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -0,0 +1,151 @@ +package com.woocommerce.android.apifaker.ui.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Card +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.woocommerce.android.apifaker.models.ApiType.Custom +import com.woocommerce.android.apifaker.models.ApiType.WPApi +import com.woocommerce.android.apifaker.models.ApiType.WPCom +import com.woocommerce.android.apifaker.models.HttpMethod +import com.woocommerce.android.apifaker.models.Request +import com.woocommerce.android.apifaker.ui.Screen + +@Composable +internal fun HomeScreen( + viewModel: HomeViewModel, + navController: NavController +) { + HomeScreen( + endpoints = viewModel.endpoints.collectAsState().value, + onMockingToggleChanged = viewModel::onMockingToggleChanged, + navController = navController + ) +} + +@Composable +private fun HomeScreen( + endpoints: List, + onMockingToggleChanged: (Boolean) -> Unit = {}, + navController: NavController +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "API Faker") }, + navigationIcon = { + IconButton(onClick = navController::navigateUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + Switch(checked = true, onCheckedChange = onMockingToggleChanged) + } + ) + } + ) { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues) + .padding(16.dp) + .fillMaxSize() + ) { + if (endpoints.isNotEmpty()) { + LazyColumn { + items(endpoints) { endpoint -> + EndpointItem(endpoint, navController, Modifier.padding(vertical = 8.dp)) + } + } + } else { + Text(text = "Start by adding some endpoints") + } + + FloatingActionButton( + onClick = { navController.navigate(Screen.EndpointDetails.routeForCreation()) }, + backgroundColor = MaterialTheme.colors.primary, + contentColor = MaterialTheme.colors.onPrimary, + modifier = Modifier.align(Alignment.BottomEnd) + ) { + Icon(imageVector = Icons.Filled.Add, contentDescription = "Add endpoint") + } + } + } +} + +@Composable +private fun EndpointItem( + request: Request, + navController: NavController, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = { navController.navigate(Screen.EndpointDetails.route(request.id)) }), + elevation = 4.dp + ) { + Column(Modifier.padding(8.dp)) { + Text( + text = when (request.type) { + WPApi -> "WordPress API" + WPCom -> "WordPress.com API" + is Custom -> "Host: ${request.type.host}" + }, + style = MaterialTheme.typography.subtitle1 + ) + Text( + text = request.path, + style = MaterialTheme.typography.subtitle2 + ) + } + } +} + +@Composable +@Preview +private fun HomeScreenPreview() { + HomeScreen( + endpoints = listOf( + Request( + type = WPApi, + httpMethod = HttpMethod.GET, + path = "/wc/v3/products", + body = "", + ), + Request( + type = WPCom, + httpMethod = HttpMethod.POST, + path = "/v1.1/me/sites", + body = "" + ), + ), + navController = rememberNavController() + ) +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt new file mode 100644 index 000000000000..d381947db620 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt @@ -0,0 +1,23 @@ +package com.woocommerce.android.apifaker.ui.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.woocommerce.android.apifaker.db.EndpointDao +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +internal class HomeViewModel @Inject constructor( + endpointDao: EndpointDao +) : ViewModel() { + val endpoints = endpointDao.observeEndpoints() + .map { list -> list.map { it.request } } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) + + fun onMockingToggleChanged(enabled: Boolean) { + TODO() + } +} From 4efe69b96b2f9e6b4a8b2192961505a6146fe5da Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 7 Mar 2023 16:05:46 +0100 Subject: [PATCH 22/68] Use hilt viewmodel --- libs/apifaker/build.gradle | 1 + .../woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle index 6da1f26b093f..12c0b345f73b 100644 --- a/libs/apifaker/build.gradle +++ b/libs/apifaker/build.gradle @@ -69,6 +69,7 @@ dependencies { ksp(libs.androidx.room.compiler) implementation(libs.google.dagger.hilt.android.main) + implementation(libs.androidx.hilt.navigation.compose) ksp(libs.google.dagger.hilt.compiler) testImplementation(libs.junit) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt index eafbec1ad751..8de5c0d458cf 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -30,7 +31,7 @@ class ApiFakerHostFragment : Fragment() { startDestination = Screen.Home.route() ) { composable(Screen.Home.route()) { - HomeScreen(navController) + HomeScreen(hiltViewModel(), navController) } composable( Screen.EndpointDetails.baseRoute, From c8d9067e4ef3682e875ded4522c6dd2f405a1104 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 3 Dec 2024 16:09:49 +0100 Subject: [PATCH 23/68] Add the faker library to the app --- WooCommerce/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/build.gradle b/WooCommerce/build.gradle index 8959fc8e1020..a191bae06ec0 100644 --- a/WooCommerce/build.gradle +++ b/WooCommerce/build.gradle @@ -359,6 +359,7 @@ dependencies { implementation(libs.google.play.services.wearable) // Debug dependencies + debugImplementation project(":libs:apifaker") debugImplementation(libs.facebook.flipper.main) debugImplementation(libs.facebook.soloader) debugImplementation(libs.facebook.flipper.network.plugin) { From bd0e18236f57f0c7fcbf50cfa6181d3fb446b8ab Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 3 Dec 2024 16:14:26 +0100 Subject: [PATCH 24/68] Add an entry to developer options and handle navigation to the API faker --- .../developer/DeveloperOptionsFragment.kt | 7 +++++++ .../developer/DeveloperOptionsViewModel.kt | 19 ++++++++++++++++++- .../res/navigation/nav_graph_api_faker.xml | 10 ++++++++++ .../res/navigation/nav_graph_settings.xml | 8 +++++++- WooCommerce/src/main/res/values/strings.xml | 1 - .../android/apifaker/ui/home/HomeScreen.kt | 14 ++++++-------- 6 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsFragment.kt index 7e1af080bde4..db4df3d44c21 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsFragment.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.woocommerce.android.R import com.woocommerce.android.ui.base.BaseFragment @@ -46,6 +47,12 @@ class DeveloperOptionsFragment : BaseFragment() { selectedValue = event.selectedValue ) } + + is DeveloperOptionsViewModel.DeveloperOptionsEvents.OpenApiFaker -> { + findNavController().navigate( + DeveloperOptionsFragmentDirections.actionDeveloperOptionsFragmentToApiFaker() + ) + } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsViewModel.kt index 86ad6d59f823..36b3a1b5688d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/developer/DeveloperOptionsViewModel.kt @@ -16,6 +16,7 @@ import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.ScopedViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -81,11 +82,25 @@ class DeveloperOptionsViewModel @Inject constructor( ) } + private val apiFakerFlow = flowOf( + NonToggleableListItem( + icon = R.drawable.ic_globe, + iconTint = R.color.color_primary, + label = UiString.UiStringText("API Faker"), + key = UiString.UiStringText("API Faker"), + isEnabled = true, + onClick = { + triggerEvent(DeveloperOptionsEvents.OpenApiFaker) + } + ) + ) + val viewState = combine( simulatedCardReaderFlow, readerUpdateFrequencyFlow, interacPaymentEnabledFlow, - savedPrivacySettingsOnDialogFlow + savedPrivacySettingsOnDialogFlow, + apiFakerFlow ) { items -> DeveloperOptionsViewState( rows = items.filterNotNull() @@ -127,6 +142,8 @@ class DeveloperOptionsViewModel @Inject constructor( val options: List, var selectedValue: UpdateFrequencyUiModel, ) : DeveloperOptionsEvents() + + data object OpenApiFaker : DeveloperOptionsEvents() } data class DeveloperOptionsViewState( diff --git a/WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml b/WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml new file mode 100644 index 000000000000..b1b8a2823970 --- /dev/null +++ b/WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml @@ -0,0 +1,10 @@ + + + + diff --git a/WooCommerce/src/main/res/navigation/nav_graph_settings.xml b/WooCommerce/src/main/res/navigation/nav_graph_settings.xml index 41056b472cee..bfee1671c7cd 100644 --- a/WooCommerce/src/main/res/navigation/nav_graph_settings.xml +++ b/WooCommerce/src/main/res/navigation/nav_graph_settings.xml @@ -7,6 +7,8 @@ + + + android:label="DeveloperOptionsFragment"> + + Enable Interac Payment Enable Interac Key - diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index fc713b83b6ff..e896f2da4625 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -28,9 +28,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import com.woocommerce.android.apifaker.models.ApiType.Custom -import com.woocommerce.android.apifaker.models.ApiType.WPApi -import com.woocommerce.android.apifaker.models.ApiType.WPCom +import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.ui.Screen @@ -114,9 +112,9 @@ private fun EndpointItem( Column(Modifier.padding(8.dp)) { Text( text = when (request.type) { - WPApi -> "WordPress API" - WPCom -> "WordPress.com API" - is Custom -> "Host: ${request.type.host}" + ApiType.WPApi -> "WordPress API" + ApiType.WPCom -> "WordPress.com API" + is ApiType.Custom -> "Host: ${request.type.host}" }, style = MaterialTheme.typography.subtitle1 ) @@ -134,13 +132,13 @@ private fun HomeScreenPreview() { HomeScreen( endpoints = listOf( Request( - type = WPApi, + type = ApiType.WPApi, httpMethod = HttpMethod.GET, path = "/wc/v3/products", body = "", ), Request( - type = WPCom, + type = ApiType.WPCom, httpMethod = HttpMethod.POST, path = "/v1.1/me/sites", body = "" From f68afa286c05be37e9426b18a558b422e40ebf9f Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 7 Mar 2023 17:37:48 +0100 Subject: [PATCH 25/68] Use an app's Fragment to allow theming the UI properly --- .../android/apifaker/ApiFakerHostFragment.kt | 29 +++++++++++ .../res/navigation/nav_graph_api_faker.xml | 10 ---- .../res/navigation/nav_graph_settings.xml | 7 ++- .../apifaker/ui/ApiFakerHostFragment.kt | 49 ------------------- .../android/apifaker/ui/ApiFakerNavHost.kt | 35 +++++++++++++ 5 files changed, 69 insertions(+), 61 deletions(-) create mode 100644 WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt delete mode 100644 WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml delete mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt diff --git a/WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt b/WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt new file mode 100644 index 000000000000..da49d9903181 --- /dev/null +++ b/WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt @@ -0,0 +1,29 @@ +package com.woocommerce.android.apifaker + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import com.woocommerce.android.apifaker.ui.ApiFakerNavHost +import com.woocommerce.android.ui.base.BaseFragment +import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground +import com.woocommerce.android.ui.main.AppBarStatus + +class ApiFakerHostFragment : BaseFragment() { + override val activityAppBarStatus: AppBarStatus + get() = AppBarStatus.Hidden + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + setContent { + WooThemeWithBackground { + ApiFakerNavHost() + } + } + } + } +} diff --git a/WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml b/WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml deleted file mode 100644 index b1b8a2823970..000000000000 --- a/WooCommerce/src/main/res/navigation/nav_graph_api_faker.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/WooCommerce/src/main/res/navigation/nav_graph_settings.xml b/WooCommerce/src/main/res/navigation/nav_graph_settings.xml index bfee1671c7cd..b29db0c9c24e 100644 --- a/WooCommerce/src/main/res/navigation/nav_graph_settings.xml +++ b/WooCommerce/src/main/res/navigation/nav_graph_settings.xml @@ -7,7 +7,6 @@ - + app:destination="@id/apiFakerHostFragment" /> + diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt deleted file mode 100644 index 8de5c0d458cf..000000000000 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerHostFragment.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.woocommerce.android.apifaker.ui - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.Fragment -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import com.woocommerce.android.apifaker.ui.home.HomeScreen -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ApiFakerHostFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - - - setContent { - val navController = rememberNavController() - - NavHost( - navController = navController, - startDestination = Screen.Home.route() - ) { - composable(Screen.Home.route()) { - HomeScreen(hiltViewModel(), navController) - } - composable( - Screen.EndpointDetails.baseRoute, - arguments = listOf(navArgument("endpointId") { - type = NavType.IntType - defaultValue = -1 - }) - ) { - TODO() - } - } - } - } - } -} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt new file mode 100644 index 000000000000..7fc1216ab94d --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt @@ -0,0 +1,35 @@ +package com.woocommerce.android.apifaker.ui + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.woocommerce.android.apifaker.ui.Screen.EndpointDetails +import com.woocommerce.android.apifaker.ui.Screen.Home +import com.woocommerce.android.apifaker.ui.home.HomeScreen + +@Composable +fun ApiFakerNavHost() { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = Home.route() + ) { + composable(Home.route()) { + HomeScreen(hiltViewModel(), navController) + } + composable( + EndpointDetails.baseRoute, + arguments = listOf(navArgument("endpointId") { + type = NavType.LongType + defaultValue = -1 + }) + ) { + TODO() + } + } +} From 8c8af90a88cdbd82795a74684005c6a9b0f69926 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 17:02:15 +0100 Subject: [PATCH 26/68] Add a way to enable/disable the API Faker --- .../android/apifaker/ApiFakerConfig.kt | 22 +++++++++++++++++++ .../android/apifaker/ui/home/HomeScreen.kt | 5 ++++- .../android/apifaker/ui/home/HomeViewModel.kt | 8 +++++-- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt new file mode 100644 index 000000000000..24f631db9f5d --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt @@ -0,0 +1,22 @@ +package com.woocommerce.android.apifaker + +import com.woocommerce.android.apifaker.db.EndpointDao +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ApiFakerConfig @Inject constructor() { + @Inject + internal lateinit var endpointDao: EndpointDao + + private val _enabled: MutableStateFlow = MutableStateFlow(false) + val enabled = _enabled.asStateFlow() + .map { it && endpointDao.endpointsCount() > 0 } + + internal fun setStatus(enabled: Boolean) { + _enabled.value = enabled + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index e896f2da4625..22115ddf457a 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -40,6 +40,7 @@ internal fun HomeScreen( ) { HomeScreen( endpoints = viewModel.endpoints.collectAsState().value, + isEnabled = viewModel.isEnabled.collectAsState(initial = false).value, onMockingToggleChanged = viewModel::onMockingToggleChanged, navController = navController ) @@ -48,6 +49,7 @@ internal fun HomeScreen( @Composable private fun HomeScreen( endpoints: List, + isEnabled: Boolean, onMockingToggleChanged: (Boolean) -> Unit = {}, navController: NavController ) { @@ -64,7 +66,7 @@ private fun HomeScreen( } }, actions = { - Switch(checked = true, onCheckedChange = onMockingToggleChanged) + Switch(checked = isEnabled, onCheckedChange = onMockingToggleChanged) } ) } @@ -144,6 +146,7 @@ private fun HomeScreenPreview() { body = "" ), ), + isEnabled = true, navController = rememberNavController() ) } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt index d381947db620..ed308ee1beb2 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt @@ -2,6 +2,7 @@ package com.woocommerce.android.apifaker.ui.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.woocommerce.android.apifaker.ApiFakerConfig import com.woocommerce.android.apifaker.db.EndpointDao import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted @@ -11,13 +12,16 @@ import javax.inject.Inject @HiltViewModel internal class HomeViewModel @Inject constructor( - endpointDao: EndpointDao + endpointDao: EndpointDao, + private val config: ApiFakerConfig ) : ViewModel() { val endpoints = endpointDao.observeEndpoints() .map { list -> list.map { it.request } } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) + val isEnabled = config.enabled + fun onMockingToggleChanged(enabled: Boolean) { - TODO() + config.setStatus(enabled) } } From 4d1b579ad527f196d2012e3078772081e750cf28 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 18:07:55 +0100 Subject: [PATCH 27/68] Minor improvements to the home screen's UI --- .../android/apifaker/ui/home/HomeScreen.kt | 74 +++++++++++++------ .../android/apifaker/ui/home/HomeViewModel.kt | 2 - 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index 22115ddf457a..c265aa635500 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -1,8 +1,12 @@ package com.woocommerce.android.apifaker.ui.home import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -30,7 +34,9 @@ import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod +import com.woocommerce.android.apifaker.models.MockedEndpoint import com.woocommerce.android.apifaker.models.Request +import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.Screen @Composable @@ -48,7 +54,7 @@ internal fun HomeScreen( @Composable private fun HomeScreen( - endpoints: List, + endpoints: List, isEnabled: Boolean, onMockingToggleChanged: (Boolean) -> Unit = {}, navController: NavController @@ -101,28 +107,42 @@ private fun HomeScreen( @Composable private fun EndpointItem( - request: Request, + endpoint: MockedEndpoint, navController: NavController, modifier: Modifier = Modifier ) { Card( modifier = modifier .fillMaxWidth() - .clickable(onClick = { navController.navigate(Screen.EndpointDetails.route(request.id)) }), + .clickable(onClick = { navController.navigate(Screen.EndpointDetails.route(endpoint.request.id)) }), elevation = 4.dp ) { - Column(Modifier.padding(8.dp)) { - Text( - text = when (request.type) { - ApiType.WPApi -> "WordPress API" - ApiType.WPCom -> "WordPress.com API" - is ApiType.Custom -> "Host: ${request.type.host}" - }, - style = MaterialTheme.typography.subtitle1 + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(8.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = when (endpoint.request.type) { + ApiType.WPApi -> "WordPress API" + ApiType.WPCom -> "WordPress.com API" + is ApiType.Custom -> "Host: ${endpoint.request.type.host}" + }, + style = MaterialTheme.typography.subtitle1 + ) + Text( + text = endpoint.request.path, + style = MaterialTheme.typography.body1 + ) + } + Spacer( + modifier = Modifier + .weight(1f) + .defaultMinSize(minWidth = 16.dp) ) Text( - text = request.path, - style = MaterialTheme.typography.subtitle2 + text = endpoint.response.statusCode.toString(), + style = MaterialTheme.typography.subtitle1 ) } } @@ -133,18 +153,24 @@ private fun EndpointItem( private fun HomeScreenPreview() { HomeScreen( endpoints = listOf( - Request( - type = ApiType.WPApi, - httpMethod = HttpMethod.GET, - path = "/wc/v3/products", - body = "", - ), - Request( - type = ApiType.WPCom, - httpMethod = HttpMethod.POST, - path = "/v1.1/me/sites", - body = "" + MockedEndpoint( + Request( + type = ApiType.WPApi, + httpMethod = HttpMethod.GET, + path = "/wc/v3/products", + body = "" + ), + Response(statusCode = 200, body = "") ), + MockedEndpoint( + Request( + type = ApiType.WPCom, + httpMethod = HttpMethod.GET, + path = "/v1.1/me/sites", + body = "" + ), + Response(statusCode = 404, body = "") + ) ), isEnabled = true, navController = rememberNavController() diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt index ed308ee1beb2..cf3d91532a23 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt @@ -6,7 +6,6 @@ import com.woocommerce.android.apifaker.ApiFakerConfig import com.woocommerce.android.apifaker.db.EndpointDao import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -16,7 +15,6 @@ internal class HomeViewModel @Inject constructor( private val config: ApiFakerConfig ) : ViewModel() { val endpoints = endpointDao.observeEndpoints() - .map { list -> list.map { it.request } } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) val isEnabled = config.enabled From 4ebb93785f0c3acb41a968fd1af0be1842e136a3 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 12:31:59 +0100 Subject: [PATCH 28/68] Improve navigation --- .../android/apifaker/ApiFakerHostFragment.kt | 5 ++++- .../android/apifaker/ui/ApiFakerNavHost.kt | 11 ++++++----- .../android/apifaker/ui/home/HomeScreen.kt | 17 +++++++++++------ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt b/WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt index da49d9903181..8bc1b3a355e3 100644 --- a/WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt +++ b/WooCommerce/src/debug/kotlin/com/woocommerce/android/apifaker/ApiFakerHostFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.navigation.findNavController import com.woocommerce.android.apifaker.ui.ApiFakerNavHost import com.woocommerce.android.ui.base.BaseFragment import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground @@ -21,7 +22,9 @@ class ApiFakerHostFragment : BaseFragment() { setContent { WooThemeWithBackground { - ApiFakerNavHost() + ApiFakerNavHost( + onExit = { findNavController().navigateUp() } + ) } } } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt index 7fc1216ab94d..42dae8af4cae 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt @@ -8,19 +8,20 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.woocommerce.android.apifaker.ui.Screen.EndpointDetails -import com.woocommerce.android.apifaker.ui.Screen.Home import com.woocommerce.android.apifaker.ui.home.HomeScreen @Composable -fun ApiFakerNavHost() { +fun ApiFakerNavHost( + onExit: () -> Unit +) { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Home.route() + startDestination = Screen.Home.route() ) { - composable(Home.route()) { - HomeScreen(hiltViewModel(), navController) + composable(Screen.Home.route()) { + HomeScreen(viewModel = hiltViewModel(), navController = navController, onExit = onExit) } composable( EndpointDetails.baseRoute, diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index c265aa635500..20cfa05035ae 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -42,13 +42,15 @@ import com.woocommerce.android.apifaker.ui.Screen @Composable internal fun HomeScreen( viewModel: HomeViewModel, - navController: NavController + navController: NavController, + onExit: () -> Unit ) { HomeScreen( endpoints = viewModel.endpoints.collectAsState().value, isEnabled = viewModel.isEnabled.collectAsState(initial = false).value, + navController = navController, onMockingToggleChanged = viewModel::onMockingToggleChanged, - navController = navController + onExit = onExit ) } @@ -56,15 +58,16 @@ internal fun HomeScreen( private fun HomeScreen( endpoints: List, isEnabled: Boolean, - onMockingToggleChanged: (Boolean) -> Unit = {}, - navController: NavController + navController: NavController, + onMockingToggleChanged: (Boolean) -> Unit, + onExit: () -> Unit, ) { Scaffold( topBar = { TopAppBar( title = { Text(text = "API Faker") }, navigationIcon = { - IconButton(onClick = navController::navigateUp) { + IconButton(onClick = onExit) { Icon( Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back" @@ -173,6 +176,8 @@ private fun HomeScreenPreview() { ) ), isEnabled = true, - navController = rememberNavController() + navController = rememberNavController(), + onMockingToggleChanged = {}, + onExit = {} ) } From 29f734dbeed8567b3bbc626f89271f01b9f5ce05 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 16:06:30 +0100 Subject: [PATCH 29/68] Match the TopAppBar background color to the app theme --- .../com/woocommerce/android/apifaker/ui/home/HomeScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index 20cfa05035ae..aff58c7b0c03 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -76,7 +76,8 @@ private fun HomeScreen( }, actions = { Switch(checked = isEnabled, onCheckedChange = onMockingToggleChanged) - } + }, + backgroundColor = MaterialTheme.colors.surface ) } ) { paddingValues -> From 82c3c214b18ef434168f3ffb7a979bec67b74da7 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 16:09:06 +0100 Subject: [PATCH 30/68] Fix detekt issue --- .../woocommerce/android/apifaker/ui/ApiFakerNavHost.kt | 10 ++++++---- .../android/apifaker/ui/home/HomeViewModel.kt | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt index 42dae8af4cae..05ebb0a22874 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt @@ -25,10 +25,12 @@ fun ApiFakerNavHost( } composable( EndpointDetails.baseRoute, - arguments = listOf(navArgument("endpointId") { - type = NavType.LongType - defaultValue = -1 - }) + arguments = listOf( + navArgument("endpointId") { + type = NavType.LongType + defaultValue = -1 + } + ) ) { TODO() } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt index cf3d91532a23..dcb81ee703b9 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt @@ -14,6 +14,7 @@ internal class HomeViewModel @Inject constructor( endpointDao: EndpointDao, private val config: ApiFakerConfig ) : ViewModel() { + @Suppress("MagicNumber") val endpoints = endpointDao.observeEndpoints() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) From c2b1f840cb5f9bc26370cc3d5ce12db92d015e39 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 12:52:43 +0100 Subject: [PATCH 31/68] Persist the ApiFaker status --- .../android/apifaker/ApiFakerConfig.kt | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt index 24f631db9f5d..d7526e82ea89 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt @@ -1,22 +1,47 @@ package com.woocommerce.android.apifaker +import android.content.Context +import android.content.SharedPreferences import com.woocommerce.android.apifaker.db.EndpointDao -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import javax.inject.Inject import javax.inject.Singleton +private const val PREF_FILE_NAME = "api_faker" +private const val PREFERENCE_KEY = "api_faker_enabled" + @Singleton -class ApiFakerConfig @Inject constructor() { - @Inject - internal lateinit var endpointDao: EndpointDao +internal class ApiFakerConfig @Inject constructor( + context: Context, + private val endpointDao: EndpointDao +) { + private val configScope = CoroutineScope(Dispatchers.Main) + private val preferences = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE) + + private val prefFlow = preferences.prefFlow(PREFERENCE_KEY, false) - private val _enabled: MutableStateFlow = MutableStateFlow(false) - val enabled = _enabled.asStateFlow() - .map { it && endpointDao.endpointsCount() > 0 } + val enabled = prefFlow.map { + it && !endpointDao.isEmpty() + }.stateIn(configScope, SharingStarted.WhileSubscribed(), false) + + fun setStatus(enabled: Boolean) { + preferences.edit().putBoolean(PREFERENCE_KEY, enabled).apply() + } - internal fun setStatus(enabled: Boolean) { - _enabled.value = enabled + private fun SharedPreferences.prefFlow(key: String, defaultValue: Boolean) = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, listenerKey -> + if (listenerKey == key) { + trySend(getBoolean(key, defaultValue)) + } + } + registerOnSharedPreferenceChangeListener(listener) + trySend(getBoolean(key, defaultValue)) + awaitClose { unregisterOnSharedPreferenceChangeListener(listener) } } } From 9dd1b5de315567ad1cffbe8d7dd8d046f19cd7d4 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 22:45:06 +0100 Subject: [PATCH 32/68] Start config flow eagerly Otherwise we won't get the preference value --- .../java/com/woocommerce/android/apifaker/ApiFakerConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt index d7526e82ea89..541ccf5f9374 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt @@ -28,7 +28,7 @@ internal class ApiFakerConfig @Inject constructor( val enabled = prefFlow.map { it && !endpointDao.isEmpty() - }.stateIn(configScope, SharingStarted.WhileSubscribed(), false) + }.stateIn(configScope, SharingStarted.Eagerly, false) fun setStatus(enabled: Boolean) { preferences.edit().putBoolean(PREFERENCE_KEY, enabled).apply() From 9b2d033b9e7e2d21f909670d40b6b2d7a480440b Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 22:53:27 +0100 Subject: [PATCH 33/68] Fix issue with updating the faker config --- .../com/woocommerce/android/apifaker/ApiFakerConfig.kt | 10 +++++++--- .../com/woocommerce/android/apifaker/db/EndpointDao.kt | 4 +--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt index 541ccf5f9374..7506b7cd9383 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -19,15 +20,18 @@ private const val PREFERENCE_KEY = "api_faker_enabled" @Singleton internal class ApiFakerConfig @Inject constructor( context: Context, - private val endpointDao: EndpointDao + endpointDao: EndpointDao ) { private val configScope = CoroutineScope(Dispatchers.Main) private val preferences = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE) private val prefFlow = preferences.prefFlow(PREFERENCE_KEY, false) - val enabled = prefFlow.map { - it && !endpointDao.isEmpty() + val enabled = combine( + prefFlow, + endpointDao.observeEndpointsCount().map { it == 0 } + ) { pref, isEmpty -> + pref && !isEmpty }.stateIn(configScope, SharingStarted.Eagerly, false) fun setStatus(enabled: Boolean) { diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index 1c1c47d0c993..e3efbd08124e 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -20,7 +20,7 @@ internal interface EndpointDao { fun observeEndpoints(): Flow> @Query("Select COUNT(*) FROM Request") - suspend fun endpointsCount(): Int + fun observeEndpointsCount(): Flow @Transaction @Query( @@ -51,6 +51,4 @@ internal interface EndpointDao { val id = insertRequest(request) insertResponse(response.copy(endpointId = id)) } - - suspend fun isEmpty() = endpointsCount() == 0 } From a25dfe5a40eefbc3047237b708dcee6e2a7698a9 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 5 Dec 2024 10:52:35 +0100 Subject: [PATCH 34/68] Remove explicit argument type --- .../java/com/woocommerce/android/apifaker/ApiFakerConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt index 7506b7cd9383..a666c1182d99 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerConfig.kt @@ -38,7 +38,7 @@ internal class ApiFakerConfig @Inject constructor( preferences.edit().putBoolean(PREFERENCE_KEY, enabled).apply() } - private fun SharedPreferences.prefFlow(key: String, defaultValue: Boolean) = callbackFlow { + private fun SharedPreferences.prefFlow(key: String, defaultValue: Boolean) = callbackFlow { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, listenerKey -> if (listenerKey == key) { trySend(getBoolean(key, defaultValue)) From f56d3ac11fa748f8e297c6b30aef03104ad227d5 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 7 Mar 2023 19:00:06 +0100 Subject: [PATCH 35/68] Start working on the details screen --- .../android/apifaker/ui/ApiFakerNavHost.kt | 7 +- .../android/apifaker/ui/DropDownMenu.kt | 60 +++++++ .../woocommerce/android/apifaker/ui/Screen.kt | 3 +- .../ui/details/EndpointDetailsScreen.kt | 147 ++++++++++++++++++ .../ui/details/EndpointDetailsViewModel.kt | 61 ++++++++ 5 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt index 05ebb0a22874..5ea278ab2672 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt @@ -7,7 +7,8 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import com.woocommerce.android.apifaker.ui.Screen.EndpointDetails +import com.woocommerce.android.apifaker.ui.Screen +import com.woocommerce.android.apifaker.ui.details.EndpointDetailsScreen import com.woocommerce.android.apifaker.ui.home.HomeScreen @Composable @@ -24,7 +25,7 @@ fun ApiFakerNavHost( HomeScreen(viewModel = hiltViewModel(), navController = navController, onExit = onExit) } composable( - EndpointDetails.baseRoute, + Screen.EndpointDetails.baseRoute, arguments = listOf( navArgument("endpointId") { type = NavType.LongType @@ -32,7 +33,7 @@ fun ApiFakerNavHost( } ) ) { - TODO() + EndpointDetailsScreen(hiltViewModel(), navController) } } } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt new file mode 100644 index 000000000000..57d5025fe686 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt @@ -0,0 +1,60 @@ +package com.woocommerce.android.apifaker.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExposedDropdownMenuBox +import androidx.compose.material.ExposedDropdownMenuDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun DropDownMenu( + label: String, + currentValue: T, + values: List, + onValueChange: (T) -> Unit, + modifier: Modifier = Modifier, + formatter: (T) -> String = { it.toString() }, +) { + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = modifier + ) { + TextField( + readOnly = true, + label = { Text(label) }, + value = formatter(currentValue), onValueChange = {}, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + modifier = Modifier.fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + values.forEach { value -> + DropdownMenuItem(onClick = { + expanded = false + onValueChange(value) + }) { + Text(formatter(value)) + } + } + } + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt index bbcf0bf0a301..568e4adcdade 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt @@ -6,7 +6,8 @@ internal sealed class Screen(val baseRoute: String) { } object EndpointDetails : Screen("/endpoint-details") { - fun route(endpointId: Long) = "$baseRoute/$endpointId" + const val endpointIdArgumentName = "endpointId" + fun route(endpointId: Long) = "$baseRoute/endpointId=$endpointId" fun routeForCreation() = baseRoute } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt new file mode 100644 index 000000000000..a6b41b43ca70 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -0,0 +1,147 @@ +package com.woocommerce.android.apifaker.ui.details + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TextField +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.MockedEndpoint +import com.woocommerce.android.apifaker.models.Request +import com.woocommerce.android.apifaker.models.Response +import com.woocommerce.android.apifaker.ui.DropDownMenu + +@Composable +internal fun EndpointDetailsScreen( + viewModel: EndpointDetailsViewModel, + navController: NavController +) { + EndpointDetailsScreen( + state = viewModel.state, + navController = navController, + onEndpointTypeChanged = viewModel::onEndpointTypeChanged + ) +} + +@Composable +private fun EndpointDetailsScreen( + state: MockedEndpoint, + navController: NavController, + onEndpointTypeChanged: (ApiType) -> Unit = {}, + onPathChanged: (String) -> Unit = {}, + onBodyChanged: (String) -> Unit = {} +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "Endpoint Definition") }, + navigationIcon = { + IconButton(onClick = navController::navigateUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + TextButton(onClick = { /*TODO*/ }) { + Text(text = "Save") + } + } + ) + }, + backgroundColor = MaterialTheme.colors.surface + ) { paddingValues -> + Column( + Modifier + .padding(paddingValues) + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + RequestDefinitionSection( + request = state.request, + onEndpointTypeChanged = onEndpointTypeChanged, + onPathChanged = onPathChanged, + onBodyChanged = onBodyChanged, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Composable +private fun RequestDefinitionSection( + request: Request, + onEndpointTypeChanged: (ApiType) -> Unit, + onPathChanged: (String) -> Unit, + onBodyChanged: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + DropDownMenu( + label = "Type", + currentValue = request.type, + values = ApiType.defaultValues(), + onValueChange = onEndpointTypeChanged, + formatter = ApiType::label, + modifier = Modifier.fillMaxWidth() + ) + if (request.type is ApiType.Custom) { + TextField( + label = { Text(text = "Host (without scheme)") }, + value = request.type.host, + onValueChange = { onEndpointTypeChanged(request.type.copy(host = it)) }, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +private val ApiType.label + get() = when (this) { + ApiType.WPApi -> "WordPress REST API" + ApiType.WPCom -> "WordPress.com REST API" + is ApiType.Custom -> "Custom" + } + +@Composable +@Preview +private fun EndpointDetailsScreenPreview() { + Surface(color = MaterialTheme.colors.background) { + EndpointDetailsScreen( + state = MockedEndpoint( + request = Request( + type = ApiType.Custom("https://example.com"), + path = "/wc/v3/products", + body = "%" + ), + response = Response( + statusCode = 200, + body = "" + ) + ), + navController = rememberNavController() + ) + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt new file mode 100644 index 000000000000..063199024cab --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -0,0 +1,61 @@ +package com.woocommerce.android.apifaker.ui.details + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot.Companion.withMutableSnapshot +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.woocommerce.android.apifaker.db.EndpointDao +import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.MockedEndpoint +import com.woocommerce.android.apifaker.models.Request +import com.woocommerce.android.apifaker.models.Response +import com.woocommerce.android.apifaker.ui.Screen +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +const val MISSING_ENDPOINT_ID = -1L + +@HiltViewModel +internal class EndpointDetailsViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val endpointDao: EndpointDao +) : ViewModel() { + private val id = savedStateHandle.get(Screen.EndpointDetails.endpointIdArgumentName)!! + + var state: MockedEndpoint by mutableStateOf(defaultEndpoint()) + private set + + init { + if (id != MISSING_ENDPOINT_ID && state.request.id == MISSING_ENDPOINT_ID) { + loadEndpoint() + } + } + + fun onEndpointTypeChanged(endpointType: ApiType) { + withMutableSnapshot { + state = state.copy(request = state.request.copy(type = endpointType)) + } + } + + private fun loadEndpoint() = viewModelScope.launch { + state = endpointDao.getEndpoint(id)!! + } + + private fun defaultEndpoint() = MockedEndpoint( + Request( + id = MISSING_ENDPOINT_ID, + type = ApiType.WPApi, + path = "", + body = "%" + ), + Response( + endpointId = MISSING_ENDPOINT_ID, + statusCode = 200, + body = "" + ) + ) +} From bedf8494f6815ea487276cfbf58e6d46770801fc Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 15:10:23 +0100 Subject: [PATCH 36/68] Handle remaining fields --- .../ui/details/EndpointDetailsScreen.kt | 344 ++++++++++++++++-- .../ui/details/EndpointDetailsViewModel.kt | 24 ++ 2 files changed, 342 insertions(+), 26 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index a6b41b43ca70..0bc0dd9d70f0 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -1,13 +1,21 @@ package com.woocommerce.android.apifaker.ui.details +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Chip +import androidx.compose.material.ChipDefaults +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface @@ -18,9 +26,21 @@ import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.woocommerce.android.apifaker.models.ApiType @@ -28,6 +48,7 @@ import com.woocommerce.android.apifaker.models.MockedEndpoint import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.DropDownMenu +import kotlin.math.min @Composable internal fun EndpointDetailsScreen( @@ -37,7 +58,11 @@ internal fun EndpointDetailsScreen( EndpointDetailsScreen( state = viewModel.state, navController = navController, - onEndpointTypeChanged = viewModel::onEndpointTypeChanged + onEndpointTypeChanged = viewModel::onEndpointTypeChanged, + onRequestPathChanged = viewModel::onRequestPathChanged, + onRequestBodyChanged = viewModel::onRequestBodyChanged, + onResponseStatusCodeChanged = viewModel::onResponseStatusCodeChanged, + onResponseBodyChanged = viewModel::onResponseBodyChanged, ) } @@ -46,8 +71,10 @@ private fun EndpointDetailsScreen( state: MockedEndpoint, navController: NavController, onEndpointTypeChanged: (ApiType) -> Unit = {}, - onPathChanged: (String) -> Unit = {}, - onBodyChanged: (String) -> Unit = {} + onRequestPathChanged: (String) -> Unit = {}, + onRequestBodyChanged: (String) -> Unit = {}, + onResponseStatusCodeChanged: (Int) -> Unit = {}, + onResponseBodyChanged: (String) -> Unit = {}, ) { Scaffold( topBar = { @@ -73,15 +100,29 @@ private fun EndpointDetailsScreen( Column( Modifier .padding(paddingValues) - .padding(16.dp) .verticalScroll(rememberScrollState()) ) { RequestDefinitionSection( request = state.request, onEndpointTypeChanged = onEndpointTypeChanged, - onPathChanged = onPathChanged, - onBodyChanged = onBodyChanged, - modifier = Modifier.fillMaxWidth() + onPathChanged = onRequestPathChanged, + onBodyChanged = onRequestBodyChanged, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) + Divider( + Modifier.padding(horizontal = 8.dp), + thickness = 2.dp + ) + + ResponseSection( + response = state.response, + onStatusCodeChanged = onResponseStatusCodeChanged, + onBodyChanged = onResponseBodyChanged, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) ) } } @@ -96,35 +137,286 @@ private fun RequestDefinitionSection( modifier: Modifier = Modifier ) { Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = modifier ) { - DropDownMenu( - label = "Type", - currentValue = request.type, - values = ApiType.defaultValues(), - onValueChange = onEndpointTypeChanged, - formatter = ApiType::label, + Text( + text = "Endpoint Conditions", + style = MaterialTheme.typography.h6 + ) + EndpointTypeField( + endpointType = request.type, + onEndpointTypeChanged = onEndpointTypeChanged, + modifier = Modifier.fillMaxWidth() + ) + + PathField( + path = request.path, + endpointType = request.type, + onPathChanged = onPathChanged, + modifier = Modifier.fillMaxWidth() + ) + + RequestBodyField( + body = request.body, + onBodyChanged = onBodyChanged, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Composable +private fun ResponseSection( + response: Response, + onStatusCodeChanged: (Int) -> Unit, + onBodyChanged: (String) -> Unit, + modifier: Modifier +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + Text( + text = "Response", + style = MaterialTheme.typography.h6 + ) + StatusCodeField( + statusCode = response.statusCode, + onStatusCodeChanged = onStatusCodeChanged, + modifier = Modifier.fillMaxWidth() + ) + ResponseBodyField( + body = response.body, + onBodyChanged = onBodyChanged, modifier = Modifier.fillMaxWidth() ) - if (request.type is ApiType.Custom) { - TextField( - label = { Text(text = "Host (without scheme)") }, - value = request.type.host, - onValueChange = { onEndpointTypeChanged(request.type.copy(host = it)) }, - modifier = Modifier.fillMaxWidth() - ) - } } } -private val ApiType.label - get() = when (this) { +@Composable +private fun EndpointTypeField( + endpointType: ApiType, + onEndpointTypeChanged: (ApiType) -> Unit, + modifier: Modifier = Modifier +) { + fun ApiType.label() = when (this) { ApiType.WPApi -> "WordPress REST API" ApiType.WPCom -> "WordPress.com REST API" is ApiType.Custom -> "Custom" } + DropDownMenu( + label = "Type", + currentValue = endpointType, + values = ApiType.defaultValues(), + onValueChange = onEndpointTypeChanged, + formatter = ApiType::label, + modifier = modifier.fillMaxWidth() + ) + if (endpointType is ApiType.Custom) { + TextField( + label = { Text(text = "Host (without scheme)") }, + value = endpointType.host, + onValueChange = { onEndpointTypeChanged(endpointType.copy(host = it)) }, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Composable +private fun PathField( + path: String, + endpointType: ApiType, + onPathChanged: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier) { + TextField( + label = { Text(text = "Path") }, + value = path, + onValueChange = onPathChanged, + modifier = Modifier.fillMaxWidth() + ) + val prefix = when (endpointType) { + ApiType.WPApi -> "/wp-json" + ApiType.WPCom -> "/rest" + is ApiType.Custom -> "host" + } + val caption = buildAnnotatedString { + append("Enter the path after the") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(" $prefix ") + } + append("part, without the query arguments") + append("\n") + append("Use") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(" % ") + } + append("as a wildcard for one or more characters") + } + Text( + text = caption, + style = MaterialTheme.typography.caption + ) + } +} + +@Composable +private fun RequestBodyField( + body: String?, + onBodyChanged: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier) { + TextField( + label = { Text(text = "Body") }, + value = body.orEmpty(), + placeholder = { Text(text = "An empty value will match everything") }, + textStyle = if (body != null) LocalTextStyle.current + else LocalTextStyle.current.copy(color = Color.Gray), + onValueChange = onBodyChanged, + modifier = Modifier.fillMaxWidth() + ) + val caption = buildAnnotatedString { + append("Use") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(" % ") + } + append("as a wildcard for one or more characters") + } + Text( + text = caption, + style = MaterialTheme.typography.caption + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun StatusCodeField( + statusCode: Int, + onStatusCodeChanged: (Int) -> Unit, + modifier: Modifier +) { + @Composable + fun StatusCodeChip( + text: String, + selected: Boolean, + onClick: () -> Unit, + ) { + Chip( + onClick = onClick, + border = ChipDefaults.outlinedBorder, + colors = ChipDefaults.outlinedChipColors( + backgroundColor = if (selected) MaterialTheme.colors.primary else MaterialTheme.colors.surface, + contentColor = if (selected) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface + ) + ) { + Text(text = text) + } + } + + @Composable + fun CustomStatusCodeDialog( + statusCode: Int, + onDismiss: () -> Unit, + onStatusCodeChanged: (Int) -> Unit + ) { + Dialog(onDismissRequest = onDismiss) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .background(MaterialTheme.colors.surface, shape = MaterialTheme.shapes.medium) + .padding(16.dp) + ) { + var fieldValue by remember { + mutableStateOf(statusCode.toString()) + } + TextField( + label = { Text(text = "Custom status code") }, + value = fieldValue, + onValueChange = { + fieldValue = it.substring(0, min(it.length, 3)) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth() + ) + + TextButton( + onClick = { + onStatusCodeChanged(fieldValue.toInt()) + }, + enabled = fieldValue.length == 3 && fieldValue.toIntOrNull() != null, + modifier = Modifier.align(Alignment.End) + ) { + Text(text = "OK") + } + } + } + } + + val defaultStatusCodes = remember { + arrayOf(200, 403, 404, 500) + } + val isUsingACustomValue = remember(statusCode) { + !defaultStatusCodes.contains(statusCode) + } + + var showCustomStatusCodeDialog by remember { + mutableStateOf(false) + } + + Column(modifier) { + Text( + text = "Status Code", + style = MaterialTheme.typography.subtitle1 + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + defaultStatusCodes.forEach { code -> + StatusCodeChip( + text = code.toString(), + selected = statusCode == code, + onClick = { onStatusCodeChanged(code) } + ) + } + + StatusCodeChip( + text = "Custom${if (isUsingACustomValue) " ($statusCode) " else ""}", + selected = isUsingACustomValue, + onClick = { showCustomStatusCodeDialog = true } + ) + } + if (showCustomStatusCodeDialog) { + CustomStatusCodeDialog( + statusCode = statusCode, + onDismiss = { showCustomStatusCodeDialog = false }, + onStatusCodeChanged = { + showCustomStatusCodeDialog = false + onStatusCodeChanged(it) + } + ) + } + } +} + +@Composable +private fun ResponseBodyField( + body: String?, + onBodyChanged: (String) -> Unit, + modifier: Modifier +) { + TextField( + label = { Text(text = "Body") }, + value = body.orEmpty(), + onValueChange = onBodyChanged, + modifier = modifier + ) +} + @Composable @Preview private fun EndpointDetailsScreenPreview() { @@ -134,10 +426,10 @@ private fun EndpointDetailsScreenPreview() { request = Request( type = ApiType.Custom("https://example.com"), path = "/wc/v3/products", - body = "%" + body = null ), response = Response( - statusCode = 200, + statusCode = 300, body = "" ) ), diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index 063199024cab..83cef7510095 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -41,6 +41,30 @@ internal class EndpointDetailsViewModel @Inject constructor( } } + fun onRequestPathChanged(path: String) { + withMutableSnapshot { + state = state.copy(request = state.request.copy(path = path)) + } + } + + fun onRequestBodyChanged(body: String) { + withMutableSnapshot { + state = state.copy(request = state.request.copy(body = body.ifEmpty { null })) + } + } + + fun onResponseStatusCodeChanged(statusCode: Int) { + withMutableSnapshot { + state = state.copy(response = state.response.copy(statusCode = statusCode)) + } + } + + fun onResponseBodyChanged(body: String) { + withMutableSnapshot { + state = state.copy(response = state.response.copy(body = body)) + } + } + private fun loadEndpoint() = viewModelScope.launch { state = endpointDao.getEndpoint(id)!! } From 7134c74828a033340b29e5208c22c7f6d77395ec Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 15:26:41 +0100 Subject: [PATCH 37/68] Handle saving changes --- .../ui/details/EndpointDetailsScreen.kt | 22 +++++++++++---- .../ui/details/EndpointDetailsViewModel.kt | 28 ++++++++++++++++--- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index 0bc0dd9d70f0..65efa38d7273 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Chip import androidx.compose.material.ChipDefaults import androidx.compose.material.Divider @@ -44,7 +45,6 @@ import androidx.compose.ui.window.Dialog import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.woocommerce.android.apifaker.models.ApiType -import com.woocommerce.android.apifaker.models.MockedEndpoint import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.DropDownMenu @@ -55,9 +55,14 @@ internal fun EndpointDetailsScreen( viewModel: EndpointDetailsViewModel, navController: NavController ) { + if (viewModel.state.isEndpointSaved) { + navController.navigateUp() + } + EndpointDetailsScreen( state = viewModel.state, navController = navController, + onSaveClicked = viewModel::onSaveClicked, onEndpointTypeChanged = viewModel::onEndpointTypeChanged, onRequestPathChanged = viewModel::onRequestPathChanged, onRequestBodyChanged = viewModel::onRequestBodyChanged, @@ -68,8 +73,9 @@ internal fun EndpointDetailsScreen( @Composable private fun EndpointDetailsScreen( - state: MockedEndpoint, + state: EndpointDetailsViewModel.UiState, navController: NavController, + onSaveClicked: () -> Unit = {}, onEndpointTypeChanged: (ApiType) -> Unit = {}, onRequestPathChanged: (String) -> Unit = {}, onRequestBodyChanged: (String) -> Unit = {}, @@ -89,8 +95,14 @@ private fun EndpointDetailsScreen( } }, actions = { - TextButton(onClick = { /*TODO*/ }) { - Text(text = "Save") + TextButton( + onClick = onSaveClicked, + enabled = state.isEndpointValid, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.onPrimary) + ) { + Text( + text = "Save" + ) } } ) @@ -422,7 +434,7 @@ private fun ResponseBodyField( private fun EndpointDetailsScreenPreview() { Surface(color = MaterialTheme.colors.background) { EndpointDetailsScreen( - state = MockedEndpoint( + state = EndpointDetailsViewModel.UiState( request = Request( type = ApiType.Custom("https://example.com"), path = "/wc/v3/products", diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index 83cef7510095..c0c872224a6e 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType -import com.woocommerce.android.apifaker.models.MockedEndpoint import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.Screen @@ -26,7 +25,7 @@ internal class EndpointDetailsViewModel @Inject constructor( ) : ViewModel() { private val id = savedStateHandle.get(Screen.EndpointDetails.endpointIdArgumentName)!! - var state: MockedEndpoint by mutableStateOf(defaultEndpoint()) + var state: UiState by mutableStateOf(defaultEndpoint()) private set init { @@ -65,11 +64,32 @@ internal class EndpointDetailsViewModel @Inject constructor( } } + fun onSaveClicked() { + viewModelScope.launch { + endpointDao.insertEndpoint(state.request, state.response) + state = state.copy(isEndpointSaved = true) + } + } + private fun loadEndpoint() = viewModelScope.launch { - state = endpointDao.getEndpoint(id)!! + state = endpointDao.getEndpoint(id)!!.let { + UiState( + it.request, + it.response + ) + } + } + + data class UiState( + val request: Request, + val response: Response, + val isEndpointSaved: Boolean = false + ) { + val isEndpointValid: Boolean + get() = request.path.isNotBlank() } - private fun defaultEndpoint() = MockedEndpoint( + private fun defaultEndpoint() = UiState( Request( id = MISSING_ENDPOINT_ID, type = ApiType.WPApi, From b02ce897b3210e26ec67ffa729354282c2538890 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 15:52:19 +0100 Subject: [PATCH 38/68] Fixes to saving and navigation --- .../com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt | 7 ++++--- .../java/com/woocommerce/android/apifaker/ui/Screen.kt | 4 +++- .../apifaker/ui/details/EndpointDetailsViewModel.kt | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt index 5ea278ab2672..07ed53f95100 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt @@ -9,6 +9,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.woocommerce.android.apifaker.ui.Screen import com.woocommerce.android.apifaker.ui.details.EndpointDetailsScreen +import com.woocommerce.android.apifaker.ui.details.MISSING_ENDPOINT_ID import com.woocommerce.android.apifaker.ui.home.HomeScreen @Composable @@ -25,11 +26,11 @@ fun ApiFakerNavHost( HomeScreen(viewModel = hiltViewModel(), navController = navController, onExit = onExit) } composable( - Screen.EndpointDetails.baseRoute, + Screen.EndpointDetails.routeTemplate, arguments = listOf( - navArgument("endpointId") { + navArgument(Screen.EndpointDetails.endpointIdArgumentName) { type = NavType.LongType - defaultValue = -1 + defaultValue = MISSING_ENDPOINT_ID } ) ) { diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt index 568e4adcdade..306eeda07c3a 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/Screen.kt @@ -7,7 +7,9 @@ internal sealed class Screen(val baseRoute: String) { object EndpointDetails : Screen("/endpoint-details") { const val endpointIdArgumentName = "endpointId" - fun route(endpointId: Long) = "$baseRoute/endpointId=$endpointId" + val routeTemplate = "$baseRoute?$endpointIdArgumentName={$endpointIdArgumentName}" + + fun route(endpointId: Long) = "$baseRoute?$endpointIdArgumentName=$endpointId" fun routeForCreation() = baseRoute } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index c0c872224a6e..c9c04d48174e 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -16,7 +16,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject -const val MISSING_ENDPOINT_ID = -1L +const val MISSING_ENDPOINT_ID = 0L @HiltViewModel internal class EndpointDetailsViewModel @Inject constructor( From 72899dcc27d69ec2023c3b75c730407e98499893 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 15:59:57 +0100 Subject: [PATCH 39/68] Rename classes --- .../ui/details/EndpointDetailsScreen.kt | 32 +++++++++---------- .../ui/details/EndpointDetailsViewModel.kt | 6 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index 65efa38d7273..ae44fe849517 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -63,7 +63,7 @@ internal fun EndpointDetailsScreen( state = viewModel.state, navController = navController, onSaveClicked = viewModel::onSaveClicked, - onEndpointTypeChanged = viewModel::onEndpointTypeChanged, + onApiTypeChanged = viewModel::onApiTypeChanged, onRequestPathChanged = viewModel::onRequestPathChanged, onRequestBodyChanged = viewModel::onRequestBodyChanged, onResponseStatusCodeChanged = viewModel::onResponseStatusCodeChanged, @@ -76,7 +76,7 @@ private fun EndpointDetailsScreen( state: EndpointDetailsViewModel.UiState, navController: NavController, onSaveClicked: () -> Unit = {}, - onEndpointTypeChanged: (ApiType) -> Unit = {}, + onApiTypeChanged: (ApiType) -> Unit = {}, onRequestPathChanged: (String) -> Unit = {}, onRequestBodyChanged: (String) -> Unit = {}, onResponseStatusCodeChanged: (Int) -> Unit = {}, @@ -116,7 +116,7 @@ private fun EndpointDetailsScreen( ) { RequestDefinitionSection( request = state.request, - onEndpointTypeChanged = onEndpointTypeChanged, + onApiTypeChanged = onApiTypeChanged, onPathChanged = onRequestPathChanged, onBodyChanged = onRequestBodyChanged, modifier = Modifier @@ -143,7 +143,7 @@ private fun EndpointDetailsScreen( @Composable private fun RequestDefinitionSection( request: Request, - onEndpointTypeChanged: (ApiType) -> Unit, + onApiTypeChanged: (ApiType) -> Unit, onPathChanged: (String) -> Unit, onBodyChanged: (String) -> Unit, modifier: Modifier = Modifier @@ -157,14 +157,14 @@ private fun RequestDefinitionSection( style = MaterialTheme.typography.h6 ) EndpointTypeField( - endpointType = request.type, - onEndpointTypeChanged = onEndpointTypeChanged, + apiType = request.type, + onApiTypeChanged = onApiTypeChanged, modifier = Modifier.fillMaxWidth() ) PathField( path = request.path, - endpointType = request.type, + apiType = request.type, onPathChanged = onPathChanged, modifier = Modifier.fillMaxWidth() ) @@ -207,8 +207,8 @@ private fun ResponseSection( @Composable private fun EndpointTypeField( - endpointType: ApiType, - onEndpointTypeChanged: (ApiType) -> Unit, + apiType: ApiType, + onApiTypeChanged: (ApiType) -> Unit, modifier: Modifier = Modifier ) { fun ApiType.label() = when (this) { @@ -219,17 +219,17 @@ private fun EndpointTypeField( DropDownMenu( label = "Type", - currentValue = endpointType, + currentValue = apiType, values = ApiType.defaultValues(), - onValueChange = onEndpointTypeChanged, + onValueChange = onApiTypeChanged, formatter = ApiType::label, modifier = modifier.fillMaxWidth() ) - if (endpointType is ApiType.Custom) { + if (apiType is ApiType.Custom) { TextField( label = { Text(text = "Host (without scheme)") }, - value = endpointType.host, - onValueChange = { onEndpointTypeChanged(endpointType.copy(host = it)) }, + value = apiType.host, + onValueChange = { onApiTypeChanged(apiType.copy(host = it)) }, modifier = Modifier.fillMaxWidth() ) } @@ -238,7 +238,7 @@ private fun EndpointTypeField( @Composable private fun PathField( path: String, - endpointType: ApiType, + apiType: ApiType, onPathChanged: (String) -> Unit, modifier: Modifier = Modifier ) { @@ -249,7 +249,7 @@ private fun PathField( onValueChange = onPathChanged, modifier = Modifier.fillMaxWidth() ) - val prefix = when (endpointType) { + val prefix = when (apiType) { ApiType.WPApi -> "/wp-json" ApiType.WPCom -> "/rest" is ApiType.Custom -> "host" diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index c9c04d48174e..196a67949e37 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -20,7 +20,7 @@ const val MISSING_ENDPOINT_ID = 0L @HiltViewModel internal class EndpointDetailsViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, private val endpointDao: EndpointDao ) : ViewModel() { private val id = savedStateHandle.get(Screen.EndpointDetails.endpointIdArgumentName)!! @@ -34,9 +34,9 @@ internal class EndpointDetailsViewModel @Inject constructor( } } - fun onEndpointTypeChanged(endpointType: ApiType) { + fun onApiTypeChanged(apiType: ApiType) { withMutableSnapshot { - state = state.copy(request = state.request.copy(type = endpointType)) + state = state.copy(request = state.request.copy(type = apiType)) } } From 7387eecd194214b9a09222156b462b0378f11388 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 16:48:29 +0100 Subject: [PATCH 40/68] Improve the query statement --- .../android/apifaker/ui/details/EndpointDetailsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index 196a67949e37..8fac554aca84 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -94,7 +94,7 @@ internal class EndpointDetailsViewModel @Inject constructor( id = MISSING_ENDPOINT_ID, type = ApiType.WPApi, path = "", - body = "%" + body = null ), Response( endpointId = MISSING_ENDPOINT_ID, From e4ff1bee63db2e50b3e96d00d123f636fdc1d2f9 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 16:48:36 +0100 Subject: [PATCH 41/68] Minor improvements --- .../com/woocommerce/android/apifaker/ApiFakerInterceptor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt index 6a3c8602bcaf..891fd75d0788 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt @@ -22,7 +22,7 @@ internal class ApiFakerInterceptor @Inject constructor(private val endpointProce } return if (fakeResponse != null) { - Log.d(LOG_TAG, "Fake request: ${chain.request().url}:\n$fakeResponse") + Log.d(LOG_TAG, "Matched request: ${chain.request().url}:\nSending Mocked Response: $fakeResponse") Response.Builder() .request(request) .protocol(Protocol.HTTP_1_1) From 978c39843d09a4157e16f6d6371feec0a149b3ab Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 18:10:54 +0100 Subject: [PATCH 42/68] Consume the status configuration --- .../android/apifaker/ApiFakerInterceptor.kt | 11 ++++++++++- .../android/apifaker/di/ApiFakerModule.kt | 15 +++++++++------ .../android/apifaker/ui/home/HomeViewModel.kt | 5 ++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt index 891fd75d0788..4534b4d541f1 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt @@ -1,6 +1,8 @@ package com.woocommerce.android.apifaker import android.util.Log +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Interceptor.Chain import okhttp3.MediaType.Companion.toMediaType @@ -9,9 +11,16 @@ import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import javax.inject.Inject -internal class ApiFakerInterceptor @Inject constructor(private val endpointProcessor: EndpointProcessor) : Interceptor { +internal class ApiFakerInterceptor @Inject constructor( + private val apiFakerConfig: ApiFakerConfig, + private val endpointProcessor: EndpointProcessor +) : Interceptor { @Suppress("TooGenericExceptionCaught", "SwallowedException") override fun intercept(chain: Chain): Response { + if (!apiFakerConfig.enabled.value) { + return chain.proceed(chain.request()) + } + Log.d(LOG_TAG, "Intercepting request: ${chain.request().url}") val request = chain.request() val fakeResponse = try { diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt index da3b649e3fea..06e5fed55433 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.apifaker.di import android.content.Context +import com.woocommerce.android.apifaker.ApiFakerConfig import com.woocommerce.android.apifaker.ApiFakerInterceptor import com.woocommerce.android.apifaker.EndpointProcessor import com.woocommerce.android.apifaker.db.ApiFakerDatabase @@ -23,9 +24,11 @@ object ApiFakerModule { @Provides internal fun providesEndpointDao(db: ApiFakerDatabase) = db.endpointDao - @Provides - @IntoSet - @Named("interceptors") - internal fun providesInterceptor(endpointProcessor: EndpointProcessor): Interceptor = - ApiFakerInterceptor(endpointProcessor) -} + @Provides + @IntoSet + @Named("interceptors") + internal fun providesInterceptor( + apiFakerConfig: ApiFakerConfig, + endpointProcessor: EndpointProcessor + ): Interceptor = ApiFakerInterceptor(apiFakerConfig, endpointProcessor) + } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt index dcb81ee703b9..e1ecb41ec1ea 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt @@ -7,6 +7,7 @@ import com.woocommerce.android.apifaker.db.EndpointDao import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -21,6 +22,8 @@ internal class HomeViewModel @Inject constructor( val isEnabled = config.enabled fun onMockingToggleChanged(enabled: Boolean) { - config.setStatus(enabled) + viewModelScope.launch { + config.setStatus(enabled) + } } } From 2d23d51cea9536f889261631ffebff483c285be9 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 8 Mar 2023 18:24:39 +0100 Subject: [PATCH 43/68] Add ability to delete requests --- .../android/apifaker/ui/home/HomeScreen.kt | 115 ++++++++++++------ .../android/apifaker/ui/home/HomeViewModel.kt | 9 +- 2 files changed, 89 insertions(+), 35 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index aff58c7b0c03..477a7262d291 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.apifaker.ui.home +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -13,21 +14,28 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Card +import androidx.compose.material.DismissDirection.EndToStart +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FloatingActionButton +import androidx.compose.material.FractionalThreshold import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -38,6 +46,7 @@ import com.woocommerce.android.apifaker.models.MockedEndpoint import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.Screen +import com.woocommerce.android.apifaker.ui.Screen.EndpointDetails @Composable internal fun HomeScreen( @@ -49,6 +58,7 @@ internal fun HomeScreen( endpoints = viewModel.endpoints.collectAsState().value, isEnabled = viewModel.isEnabled.collectAsState(initial = false).value, navController = navController, + onRemoveRequest = viewModel::onRemoveRequest, onMockingToggleChanged = viewModel::onMockingToggleChanged, onExit = onExit ) @@ -59,8 +69,9 @@ private fun HomeScreen( endpoints: List, isEnabled: Boolean, navController: NavController, + onRemoveRequest: (Request) -> Unit, onMockingToggleChanged: (Boolean) -> Unit, - onExit: () -> Unit, + onExit: () -> Unit ) { Scaffold( topBar = { @@ -89,8 +100,13 @@ private fun HomeScreen( ) { if (endpoints.isNotEmpty()) { LazyColumn { - items(endpoints) { endpoint -> - EndpointItem(endpoint, navController, Modifier.padding(vertical = 8.dp)) + items(endpoints, { endpoint -> endpoint.request.id }) { endpoint -> + EndpointItem( + endpoint, + onRemoveRequest = onRemoveRequest, + navController, + Modifier.padding(vertical = 8.dp) + ) } } } else { @@ -109,47 +125,77 @@ private fun HomeScreen( } } +@OptIn(ExperimentalMaterialApi::class) @Composable private fun EndpointItem( endpoint: MockedEndpoint, + onRemoveRequest: (Request) -> Unit, navController: NavController, modifier: Modifier = Modifier ) { - Card( - modifier = modifier - .fillMaxWidth() - .clickable(onClick = { navController.navigate(Screen.EndpointDetails.route(endpoint.request.id)) }), - elevation = 4.dp - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(8.dp) - ) { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text( - text = when (endpoint.request.type) { - ApiType.WPApi -> "WordPress API" - ApiType.WPCom -> "WordPress.com API" - is ApiType.Custom -> "Host: ${endpoint.request.type.host}" - }, - style = MaterialTheme.typography.subtitle1 - ) - Text( - text = endpoint.request.path, - style = MaterialTheme.typography.body1 + val dismissState = rememberDismissState { + onRemoveRequest(endpoint.request) + true + } + SwipeToDismiss( + state = dismissState, + directions = setOf(EndToStart), + dismissThresholds = { + FractionalThreshold(0.3f) + }, + modifier = modifier, + background = { + Box( + Modifier + .fillMaxSize() + .background(Color.Red, MaterialTheme.shapes.medium) + .padding(horizontal = 20.dp), + contentAlignment = Alignment.CenterEnd + ) { + Icon( + Icons.Default.Delete, + contentDescription = "Delete" ) } - Spacer( + }, + dismissContent = { + Card( modifier = Modifier - .weight(1f) - .defaultMinSize(minWidth = 16.dp) - ) - Text( - text = endpoint.response.statusCode.toString(), - style = MaterialTheme.typography.subtitle1 - ) + .fillMaxWidth() + .clickable(onClick = { navController.navigate(EndpointDetails.route(endpoint.request.id)) }), + elevation = 4.dp + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(8.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = when (endpoint.request.type) { + ApiType.WPApi -> "WordPress API" + ApiType.WPCom -> "WordPress.com API" + is ApiType.Custom -> "Host: ${endpoint.request.type.host}" + }, + style = MaterialTheme.typography.subtitle1 + ) + Text( + text = endpoint.request.path, + style = MaterialTheme.typography.body1 + ) + } + Spacer( + modifier = Modifier + .weight(1f) + .defaultMinSize(minWidth = 16.dp) + ) + Text( + text = endpoint.response.statusCode.toString(), + style = MaterialTheme.typography.subtitle1 + ) + } + } } - } + ) } @Composable @@ -178,6 +224,7 @@ private fun HomeScreenPreview() { ), isEnabled = true, navController = rememberNavController(), + onRemoveRequest = {}, onMockingToggleChanged = {}, onExit = {} ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt index e1ecb41ec1ea..dc8fdbcce419 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.woocommerce.android.apifaker.ApiFakerConfig import com.woocommerce.android.apifaker.db.EndpointDao +import com.woocommerce.android.apifaker.models.Request import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn @@ -12,7 +13,7 @@ import javax.inject.Inject @HiltViewModel internal class HomeViewModel @Inject constructor( - endpointDao: EndpointDao, + private val endpointDao: EndpointDao, private val config: ApiFakerConfig ) : ViewModel() { @Suppress("MagicNumber") @@ -26,4 +27,10 @@ internal class HomeViewModel @Inject constructor( config.setStatus(enabled) } } + + fun onRemoveRequest(request: Request) { + viewModelScope.launch { + endpointDao.deleteRequest(request) + } + } } From dacfd29e187d2a20b8085d3665056ab5aa017e5e Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 09:26:45 +0100 Subject: [PATCH 44/68] Add support for filtering by http method --- .../ui/details/EndpointDetailsScreen.kt | 29 +++++++++++++++++++ .../ui/details/EndpointDetailsViewModel.kt | 8 +++++ .../android/apifaker/ui/home/HomeScreen.kt | 20 ++++++++----- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index ae44fe849517..43f2f4c4001d 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.window.Dialog import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.DropDownMenu @@ -64,6 +65,7 @@ internal fun EndpointDetailsScreen( navController = navController, onSaveClicked = viewModel::onSaveClicked, onApiTypeChanged = viewModel::onApiTypeChanged, + onRequestHttpMethodChanged = viewModel::onRequestHttpMethodChanged, onRequestPathChanged = viewModel::onRequestPathChanged, onRequestBodyChanged = viewModel::onRequestBodyChanged, onResponseStatusCodeChanged = viewModel::onResponseStatusCodeChanged, @@ -77,6 +79,7 @@ private fun EndpointDetailsScreen( navController: NavController, onSaveClicked: () -> Unit = {}, onApiTypeChanged: (ApiType) -> Unit = {}, + onRequestHttpMethodChanged: (HttpMethod?) -> Unit = {}, onRequestPathChanged: (String) -> Unit = {}, onRequestBodyChanged: (String) -> Unit = {}, onResponseStatusCodeChanged: (Int) -> Unit = {}, @@ -117,6 +120,7 @@ private fun EndpointDetailsScreen( RequestDefinitionSection( request = state.request, onApiTypeChanged = onApiTypeChanged, + onHttpMethodChanged = onRequestHttpMethodChanged, onPathChanged = onRequestPathChanged, onBodyChanged = onRequestBodyChanged, modifier = Modifier @@ -144,6 +148,7 @@ private fun EndpointDetailsScreen( private fun RequestDefinitionSection( request: Request, onApiTypeChanged: (ApiType) -> Unit, + onHttpMethodChanged: (HttpMethod?) -> Unit, onPathChanged: (String) -> Unit, onBodyChanged: (String) -> Unit, modifier: Modifier = Modifier @@ -162,6 +167,12 @@ private fun RequestDefinitionSection( modifier = Modifier.fillMaxWidth() ) + HttpMethodField( + method = request.httpMethod, + onHttpMethodChanged = onHttpMethodChanged, + modifier = Modifier.fillMaxWidth() + ) + PathField( path = request.path, apiType = request.type, @@ -235,6 +246,23 @@ private fun EndpointTypeField( } } + +@Composable +private fun HttpMethodField( + method: HttpMethod?, + onHttpMethodChanged: (HttpMethod?) -> Unit, + modifier: Modifier = Modifier +) { + DropDownMenu( + label = "HTTP Method", + currentValue = method, + values = listOf(null) + HttpMethod.values(), + onValueChange = onHttpMethodChanged, + formatter = { it?.name ?: "Any" }, + modifier = modifier.fillMaxWidth() + ) +} + @Composable private fun PathField( path: String, @@ -437,6 +465,7 @@ private fun EndpointDetailsScreenPreview() { state = EndpointDetailsViewModel.UiState( request = Request( type = ApiType.Custom("https://example.com"), + httpMethod = HttpMethod.GET, path = "/wc/v3/products", body = null ), diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index 8fac554aca84..772749621da7 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType +import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.Screen @@ -40,6 +41,12 @@ internal class EndpointDetailsViewModel @Inject constructor( } } + fun onRequestHttpMethodChanged(httpMethod: HttpMethod?) { + withMutableSnapshot { + state = state.copy(request = state.request.copy(httpMethod = httpMethod)) + } + } + fun onRequestPathChanged(path: String) { withMutableSnapshot { state = state.copy(request = state.request.copy(path = path)) @@ -93,6 +100,7 @@ internal class EndpointDetailsViewModel @Inject constructor( Request( id = MISSING_ENDPOINT_ID, type = ApiType.WPApi, + httpMethod = null, path = "", body = null ), diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index 477a7262d291..0017894a8cca 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Card -import androidx.compose.material.DismissDirection.EndToStart +import androidx.compose.material.DismissDirection import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FloatingActionButton import androidx.compose.material.FractionalThreshold @@ -36,6 +36,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -46,7 +47,6 @@ import com.woocommerce.android.apifaker.models.MockedEndpoint import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.Screen -import com.woocommerce.android.apifaker.ui.Screen.EndpointDetails @Composable internal fun HomeScreen( @@ -133,13 +133,14 @@ private fun EndpointItem( navController: NavController, modifier: Modifier = Modifier ) { - val dismissState = rememberDismissState { + val dismissState = rememberDismissState() + + if (dismissState.isDismissed(DismissDirection.EndToStart)) { onRemoveRequest(endpoint.request) - true } SwipeToDismiss( state = dismissState, - directions = setOf(EndToStart), + directions = setOf(DismissDirection.EndToStart), dismissThresholds = { FractionalThreshold(0.3f) }, @@ -162,7 +163,7 @@ private fun EndpointItem( Card( modifier = Modifier .fillMaxWidth() - .clickable(onClick = { navController.navigate(EndpointDetails.route(endpoint.request.id)) }), + .clickable(onClick = { navController.navigate(Screen.EndpointDetails.route(endpoint.request.id)) }), elevation = 4.dp ) { Row( @@ -176,10 +177,13 @@ private fun EndpointItem( ApiType.WPCom -> "WordPress.com API" is ApiType.Custom -> "Host: ${endpoint.request.type.host}" }, - style = MaterialTheme.typography.subtitle1 + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.SemiBold ) + val pathLine = endpoint.request.httpMethod?.let { "$it " }.orEmpty() + + endpoint.request.path Text( - text = endpoint.request.path, + text = pathLine, style = MaterialTheme.typography.body1 ) } From d27d000a2b99406ca586025e8873e0bc4f97b58a Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 12:31:59 +0100 Subject: [PATCH 45/68] Improve navigation --- .../com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt index 07ed53f95100..c8b08774d35c 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt @@ -1,6 +1,8 @@ package com.woocommerce.android.apifaker.ui +import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -18,6 +20,9 @@ fun ApiFakerNavHost( ) { val navController = rememberNavController() + // This might not be very safe, but since it's just for development purposes, it should be fine + val activity = LocalContext.current as ComponentActivity + NavHost( navController = navController, startDestination = Screen.Home.route() From ea138c37178926c65736da0d11df02c91524eb3e Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 12:41:30 +0100 Subject: [PATCH 46/68] UI improvements --- .../android/apifaker/ui/DropDownMenu.kt | 6 ++--- .../ui/details/EndpointDetailsScreen.kt | 24 ++++++++++++------- .../android/apifaker/ui/home/HomeScreen.kt | 3 ++- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt index 57d5025fe686..c83f32f60641 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt @@ -5,8 +5,8 @@ import androidx.compose.material.DropdownMenuItem import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExposedDropdownMenuBox import androidx.compose.material.ExposedDropdownMenuDefaults +import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text -import androidx.compose.material.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,7 +30,7 @@ internal fun DropDownMenu( onExpandedChange = { expanded = it }, modifier = modifier ) { - TextField( + OutlinedTextField( readOnly = true, label = { Text(label) }, value = formatter(currentValue), onValueChange = {}, @@ -39,7 +39,7 @@ internal fun DropDownMenu( expanded = expanded ) }, - colors = ExposedDropdownMenuDefaults.textFieldColors(), + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), modifier = Modifier.fillMaxWidth() ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index 43f2f4c4001d..73efc0775509 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -22,7 +23,8 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.TextField +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -101,13 +103,15 @@ private fun EndpointDetailsScreen( TextButton( onClick = onSaveClicked, enabled = state.isEndpointValid, - colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.onPrimary) + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.onSurface) ) { Text( text = "Save" ) } - } + }, + backgroundColor = MaterialTheme.colors.surface, + elevation = 4.dp ) }, backgroundColor = MaterialTheme.colors.surface @@ -237,7 +241,7 @@ private fun EndpointTypeField( modifier = modifier.fillMaxWidth() ) if (apiType is ApiType.Custom) { - TextField( + OutlinedTextField( label = { Text(text = "Host (without scheme)") }, value = apiType.host, onValueChange = { onApiTypeChanged(apiType.copy(host = it)) }, @@ -271,7 +275,7 @@ private fun PathField( modifier: Modifier = Modifier ) { Column(modifier) { - TextField( + OutlinedTextField( label = { Text(text = "Path") }, value = path, onValueChange = onPathChanged, @@ -309,14 +313,15 @@ private fun RequestBodyField( modifier: Modifier = Modifier ) { Column(modifier) { - TextField( + OutlinedTextField( label = { Text(text = "Body") }, value = body.orEmpty(), placeholder = { Text(text = "An empty value will match everything") }, textStyle = if (body != null) LocalTextStyle.current else LocalTextStyle.current.copy(color = Color.Gray), onValueChange = onBodyChanged, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() ) val caption = buildAnnotatedString { append("Use") @@ -373,7 +378,7 @@ private fun StatusCodeField( var fieldValue by remember { mutableStateOf(statusCode.toString()) } - TextField( + OutlinedTextField( label = { Text(text = "Custom status code") }, value = fieldValue, onValueChange = { @@ -449,11 +454,12 @@ private fun ResponseBodyField( onBodyChanged: (String) -> Unit, modifier: Modifier ) { - TextField( + OutlinedTextField( label = { Text(text = "Body") }, value = body.orEmpty(), onValueChange = onBodyChanged, modifier = modifier + .defaultMinSize(minHeight = TextFieldDefaults.MinHeight * 2) ) } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index 0017894a8cca..eea646e545e1 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -88,7 +88,8 @@ private fun HomeScreen( actions = { Switch(checked = isEnabled, onCheckedChange = onMockingToggleChanged) }, - backgroundColor = MaterialTheme.colors.surface + backgroundColor = MaterialTheme.colors.surface, + elevation = 4.dp ) } ) { paddingValues -> From d4d47758fd2768c967c4e79b3c0d478db9a70f8b Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 12:45:19 +0100 Subject: [PATCH 47/68] Apply internal visibility to some declarations --- .../java/com/woocommerce/android/apifaker/models/HttpMethod.kt | 2 +- .../com/woocommerce/android/apifaker/util/JSONObjectProvider.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt index 722ffac7c18b..718b85e5c64b 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/HttpMethod.kt @@ -1,5 +1,5 @@ package com.woocommerce.android.apifaker.models -enum class HttpMethod { +internal enum class HttpMethod { GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, TRACE, CONNECT } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt index 6f3d05e4dba8..dd6411c4fe27 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/util/JSONObjectProvider.kt @@ -3,6 +3,6 @@ package com.woocommerce.android.apifaker.util import org.json.JSONObject import javax.inject.Inject -class JSONObjectProvider @Inject constructor() { +internal class JSONObjectProvider @Inject constructor() { fun parseString(content: String): JSONObject = JSONObject(content) } From 4f15cd480b5e2233fe1269971708dcfeebf92335 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 10 Mar 2023 11:07:14 +0100 Subject: [PATCH 48/68] Add an artifical delay --- .../com/woocommerce/android/apifaker/ApiFakerInterceptor.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt index 4534b4d541f1..2b8732113a8b 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt @@ -1,8 +1,6 @@ package com.woocommerce.android.apifaker import android.util.Log -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Interceptor.Chain import okhttp3.MediaType.Companion.toMediaType @@ -11,6 +9,8 @@ import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import javax.inject.Inject +private const val ARTIFICIAL_DELAY_MS = 500L + internal class ApiFakerInterceptor @Inject constructor( private val apiFakerConfig: ApiFakerConfig, private val endpointProcessor: EndpointProcessor @@ -32,6 +32,7 @@ internal class ApiFakerInterceptor @Inject constructor( return if (fakeResponse != null) { Log.d(LOG_TAG, "Matched request: ${chain.request().url}:\nSending Mocked Response: $fakeResponse") + Thread.sleep(ARTIFICIAL_DELAY_MS) Response.Builder() .request(request) .protocol(Protocol.HTTP_1_1) From 52e10d24ae9a2777fdf27b039bc9ddd3bef3dc5f Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 15:00:00 +0100 Subject: [PATCH 49/68] Suppress deprecation temporarily --- .../com/woocommerce/android/apifaker/ui/home/HomeScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index eea646e545e1..b1f327978c8b 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material.Card import androidx.compose.material.DismissDirection import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FloatingActionButton -import androidx.compose.material.FractionalThreshold import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -143,7 +142,8 @@ private fun EndpointItem( state = dismissState, directions = setOf(DismissDirection.EndToStart), dismissThresholds = { - FractionalThreshold(0.3f) + @Suppress("DEPRECATION") + androidx.compose.material.FractionalThreshold(0.3f) }, modifier = modifier, background = { @@ -182,7 +182,7 @@ private fun EndpointItem( fontWeight = FontWeight.SemiBold ) val pathLine = endpoint.request.httpMethod?.let { "$it " }.orEmpty() + - endpoint.request.path + endpoint.request.path Text( text = pathLine, style = MaterialTheme.typography.body1 From fe1c676bea1db00fdc5ba0930d4cadffbc29ee92 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 23:03:08 +0100 Subject: [PATCH 50/68] Fix detekt issues --- .../android/apifaker/di/ApiFakerModule.kt | 16 ++++++++-------- .../android/apifaker/ui/ApiFakerNavHost.kt | 6 ------ .../android/apifaker/ui/DropDownMenu.kt | 3 ++- .../apifaker/ui/details/EndpointDetailsScreen.kt | 10 ++++++---- .../android/apifaker/ui/home/HomeScreen.kt | 3 +-- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt index 06e5fed55433..22cb93f9af92 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/di/ApiFakerModule.kt @@ -24,11 +24,11 @@ object ApiFakerModule { @Provides internal fun providesEndpointDao(db: ApiFakerDatabase) = db.endpointDao - @Provides - @IntoSet - @Named("interceptors") - internal fun providesInterceptor( - apiFakerConfig: ApiFakerConfig, - endpointProcessor: EndpointProcessor - ): Interceptor = ApiFakerInterceptor(apiFakerConfig, endpointProcessor) - } + @Provides + @IntoSet + @Named("interceptors") + internal fun providesInterceptor( + apiFakerConfig: ApiFakerConfig, + endpointProcessor: EndpointProcessor + ): Interceptor = ApiFakerInterceptor(apiFakerConfig, endpointProcessor) +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt index c8b08774d35c..9ef7354ae7de 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/ApiFakerNavHost.kt @@ -1,15 +1,12 @@ package com.woocommerce.android.apifaker.ui -import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import com.woocommerce.android.apifaker.ui.Screen import com.woocommerce.android.apifaker.ui.details.EndpointDetailsScreen import com.woocommerce.android.apifaker.ui.details.MISSING_ENDPOINT_ID import com.woocommerce.android.apifaker.ui.home.HomeScreen @@ -20,9 +17,6 @@ fun ApiFakerNavHost( ) { val navController = rememberNavController() - // This might not be very safe, but since it's just for development purposes, it should be fine - val activity = LocalContext.current as ComponentActivity - NavHost( navController = navController, startDestination = Screen.Home.route() diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt index c83f32f60641..b1d29cd70860 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/DropDownMenu.kt @@ -33,7 +33,8 @@ internal fun DropDownMenu( OutlinedTextField( readOnly = true, label = { Text(label) }, - value = formatter(currentValue), onValueChange = {}, + value = formatter(currentValue), + onValueChange = {}, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon( expanded = expanded diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index 73efc0775509..c675f8670511 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -19,11 +19,11 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.OutlinedTextField import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons @@ -250,7 +250,6 @@ private fun EndpointTypeField( } } - @Composable private fun HttpMethodField( method: HttpMethod?, @@ -317,8 +316,11 @@ private fun RequestBodyField( label = { Text(text = "Body") }, value = body.orEmpty(), placeholder = { Text(text = "An empty value will match everything") }, - textStyle = if (body != null) LocalTextStyle.current - else LocalTextStyle.current.copy(color = Color.Gray), + textStyle = if (body != null) { + LocalTextStyle.current + } else { + LocalTextStyle.current.copy(color = Color.Gray) + }, onValueChange = onBodyChanged, modifier = Modifier .fillMaxWidth() diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index b1f327978c8b..2c6e81a5b676 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -181,8 +181,7 @@ private fun EndpointItem( style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.SemiBold ) - val pathLine = endpoint.request.httpMethod?.let { "$it " }.orEmpty() + - endpoint.request.path + val pathLine = endpoint.request.httpMethod?.let { "$it " }.orEmpty() + endpoint.request.path Text( text = pathLine, style = MaterialTheme.typography.body1 From 876c3d99ae4221dbc420019f0276bab3f0f015ca Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 5 Dec 2024 12:29:36 +0100 Subject: [PATCH 51/68] Add some padding between the text fields and their captions --- .../apifaker/ui/details/EndpointDetailsScreen.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index c675f8670511..a8a688a34c6b 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -273,7 +273,10 @@ private fun PathField( onPathChanged: (String) -> Unit, modifier: Modifier = Modifier ) { - Column(modifier) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { OutlinedTextField( label = { Text(text = "Path") }, value = path, @@ -311,7 +314,10 @@ private fun RequestBodyField( onBodyChanged: (String) -> Unit, modifier: Modifier = Modifier ) { - Column(modifier) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { OutlinedTextField( label = { Text(text = "Body") }, value = body.orEmpty(), From 33dbf0fb80a06d68a381e67cbcfb36bcbc930a0a Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 11:57:57 +0100 Subject: [PATCH 52/68] Display a hint when ApiFaker is enabled --- .../woocommerce/android/WooCommerceDebug.kt | 6 + .../android/apifaker/ApiFakerUiHelper.kt | 108 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt diff --git a/WooCommerce/src/debug/kotlin/com/woocommerce/android/WooCommerceDebug.kt b/WooCommerce/src/debug/kotlin/com/woocommerce/android/WooCommerceDebug.kt index fa3b41b9b846..bb5860acba95 100644 --- a/WooCommerce/src/debug/kotlin/com/woocommerce/android/WooCommerceDebug.kt +++ b/WooCommerce/src/debug/kotlin/com/woocommerce/android/WooCommerceDebug.kt @@ -12,13 +12,18 @@ import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin import com.facebook.flipper.plugins.network.NetworkFlipperPlugin import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin import com.facebook.soloader.SoLoader +import com.woocommerce.android.apifaker.ApiFakerUiHelper import com.woocommerce.android.util.SystemVersionUtils import com.woocommerce.android.util.WooLog import com.woocommerce.android.util.WooLog.T import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject @HiltAndroidApp class WooCommerceDebug : WooCommerce() { + @Inject + lateinit var apiFakerUiHelper: ApiFakerUiHelper + override fun onCreate() { if (FlipperUtils.shouldEnableFlipper(this)) { SoLoader.init(this, false) @@ -33,6 +38,7 @@ class WooCommerceDebug : WooCommerce() { enableWebContentDebugging() super.onCreate() enableStrictMode() + apiFakerUiHelper.attachToApplication(this) } /** diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt new file mode 100644 index 000000000000..89e922bfeb21 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt @@ -0,0 +1,108 @@ +package com.woocommerce.android.apifaker + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Application +import android.app.Application.ActivityLifecycleCallbacks +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.view.doOnLayout +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ApiFakerUiHelper @Inject constructor() : ActivityLifecycleCallbacks { + @Inject + internal lateinit var apiFakerConfig: ApiFakerConfig + + private val apiFakerHintId = View.generateViewId() + + fun attachToApplication(application: Application) { + application.registerActivityLifecycleCallbacks(this) + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + (activity as LifecycleOwner).lifecycleScope.launch { + updateApiFakerHint(WeakReference(activity)) + } + } + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) {} + + override fun onActivityPaused(activity: Activity) {} + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) {} + + private suspend fun updateApiFakerHint( + activityReference: WeakReference + ) { + apiFakerConfig.enabled.collect { enabled -> + activityReference.get()?.let { activity -> + if (enabled) { + activity.window.decorView.post { + activity.window.decorView.showApiFakerHint() + } + } else { + activity.window.decorView.post { + activity.window.decorView.hideApiFakerHint() + } + } + } + } + } + + @SuppressLint("SetTextI18n") + private fun View.showApiFakerHint() { + // This works only for activities that has the content view as a direct child of the FrameLayout, which is true + // for all AppCompat activities, so it should work for all the cases we need. + val contentLayout = findViewById(android.R.id.content) as? FrameLayout ?: return + val activityLayout = contentLayout.getChildAt(0) + + val apiFakerHint = FrameLayout(context).apply { + id = apiFakerHintId + setBackgroundColor(Color.RED) + addView( + TextView(context).apply { + text = "ApiFaker Enabled" + textAlignment = View.TEXT_ALIGNMENT_CENTER + } + ) + } + contentLayout.addView( + apiFakerHint, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ).apply { + gravity = android.view.Gravity.BOTTOM + } + ) + + apiFakerHint.doOnLayout { view -> + activityLayout.updateLayoutParams { bottomMargin = view.measuredHeight } + } + } + + private fun View.hideApiFakerHint() { + val contentLayout = findViewById(android.R.id.content) + contentLayout.findViewById(apiFakerHintId)?.let { apiFakerHint -> + contentLayout.removeView(apiFakerHint) + } + } +} From 6c5bfe93997aa3d4c6505c2a1217d834212916e1 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 13:00:03 +0100 Subject: [PATCH 53/68] Add ability to disable the ApiFaker from the UI hint --- libs/apifaker/build.gradle | 1 + .../android/apifaker/ApiFakerUiHelper.kt | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle index 12c0b345f73b..3383df24afb3 100644 --- a/libs/apifaker/build.gradle +++ b/libs/apifaker/build.gradle @@ -51,6 +51,7 @@ android { dependencies { implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) implementation(libs.androidx.navigation.compose) implementation(platform(libs.androidx.compose.bom)) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt index 89e922bfeb21..4dc2a1865003 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt @@ -11,9 +11,10 @@ import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.widget.FrameLayout import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.appcompat.app.AlertDialog import androidx.core.view.doOnLayout import androidx.core.view.updateLayoutParams -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import java.lang.ref.WeakReference @@ -32,7 +33,7 @@ class ApiFakerUiHelper @Inject constructor() : ActivityLifecycleCallbacks { } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - (activity as LifecycleOwner).lifecycleScope.launch { + (activity as? ComponentActivity)?.lifecycleScope?.launch { updateApiFakerHint(WeakReference(activity)) } } @@ -50,13 +51,13 @@ class ApiFakerUiHelper @Inject constructor() : ActivityLifecycleCallbacks { override fun onActivityDestroyed(activity: Activity) {} private suspend fun updateApiFakerHint( - activityReference: WeakReference + activityReference: WeakReference ) { apiFakerConfig.enabled.collect { enabled -> activityReference.get()?.let { activity -> if (enabled) { activity.window.decorView.post { - activity.window.decorView.showApiFakerHint() + activity.window.decorView.showApiFakerHint(activity) } } else { activity.window.decorView.post { @@ -68,7 +69,7 @@ class ApiFakerUiHelper @Inject constructor() : ActivityLifecycleCallbacks { } @SuppressLint("SetTextI18n") - private fun View.showApiFakerHint() { + private fun View.showApiFakerHint(activity: ComponentActivity) { // This works only for activities that has the content view as a direct child of the FrameLayout, which is true // for all AppCompat activities, so it should work for all the cases we need. val contentLayout = findViewById(android.R.id.content) as? FrameLayout ?: return @@ -83,6 +84,18 @@ class ApiFakerUiHelper @Inject constructor() : ActivityLifecycleCallbacks { textAlignment = View.TEXT_ALIGNMENT_CENTER } ) + setOnClickListener { + AlertDialog.Builder(context) + .setTitle("ApiFaker") + .setMessage("ApiFaker is enabled. Do you want to disable it?") + .setPositiveButton("Yes") { _, _ -> + activity.lifecycleScope.launch { + apiFakerConfig.setStatus(false) + } + } + .setNegativeButton("No") { _, _ -> } + .show() + } } contentLayout.addView( apiFakerHint, From 753426878995afb8970ba790cb59800f36321097 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 22:58:26 +0100 Subject: [PATCH 54/68] Set text color as white in the hint --- .../java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt index 4dc2a1865003..e87c377271ab 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt @@ -82,6 +82,7 @@ class ApiFakerUiHelper @Inject constructor() : ActivityLifecycleCallbacks { TextView(context).apply { text = "ApiFaker Enabled" textAlignment = View.TEXT_ALIGNMENT_CENTER + setTextColor(Color.WHITE) } ) setOnClickListener { From 1412386f12fe70983eb9f84425bf5184176e7574 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 23:00:41 +0100 Subject: [PATCH 55/68] Fix detekt issues --- .../woocommerce/android/apifaker/ApiFakerUiHelper.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt index e87c377271ab..7bb7ecbb2690 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerUiHelper.kt @@ -38,17 +38,17 @@ class ApiFakerUiHelper @Inject constructor() : ActivityLifecycleCallbacks { } } - override fun onActivityStarted(activity: Activity) {} + override fun onActivityStarted(activity: Activity) = Unit - override fun onActivityResumed(activity: Activity) {} + override fun onActivityResumed(activity: Activity) = Unit - override fun onActivityPaused(activity: Activity) {} + override fun onActivityPaused(activity: Activity) = Unit - override fun onActivityStopped(activity: Activity) {} + override fun onActivityStopped(activity: Activity) = Unit - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit - override fun onActivityDestroyed(activity: Activity) {} + override fun onActivityDestroyed(activity: Activity) = Unit private suspend fun updateApiFakerHint( activityReference: WeakReference From 43b60e24fdfe11d0cb12d1eb2d75f3a3fa58ae7a Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 13:23:26 +0100 Subject: [PATCH 56/68] Add query parameters to the DB model --- .../android/apifaker/db/ApiFakerDatabase.kt | 5 ++++- .../apifaker/db/QueryParameterConverter.kt | 19 +++++++++++++++++++ .../android/apifaker/models/QueryParameter.kt | 6 ++++++ .../android/apifaker/models/Request.kt | 5 +++-- .../android/apifaker/models/Response.kt | 2 +- 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt create mode 100644 libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/QueryParameter.kt diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt index d95829ab6353..83742eb6a1c9 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt @@ -16,7 +16,10 @@ import com.woocommerce.android.apifaker.models.Response version = 1, exportSchema = false ) -@TypeConverters(EndpointTypeConverter::class) +@TypeConverters( + EndpointTypeConverter::class, + QueryParameterConverter::class +) internal abstract class ApiFakerDatabase : RoomDatabase() { companion object { fun buildDb(applicationContext: Context) = Room diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt new file mode 100644 index 000000000000..726412c0a941 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt @@ -0,0 +1,19 @@ +package com.woocommerce.android.apifaker.db + +import androidx.room.TypeConverter +import com.woocommerce.android.apifaker.models.QueryParameter + +internal class QueryParameterConverter { + @TypeConverter + fun fromQueryParameters(queryParameters: List?): String? { + return queryParameters?.joinToString("&") { "${it.name}:${it.value}" } + } + + @TypeConverter + fun toQueryParameters(query: String?): List? { + return query?.split("&")?.map { parts -> + val (name, value) = parts.split(":") + QueryParameter(name, value) + } + } +} diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/QueryParameter.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/QueryParameter.kt new file mode 100644 index 000000000000..ebfc7e0bc5c4 --- /dev/null +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/QueryParameter.kt @@ -0,0 +1,6 @@ +package com.woocommerce.android.apifaker.models + +internal data class QueryParameter( + val name: String, + val value: String +) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt index 2f606730775c..037631e4578b 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Request.kt @@ -7,7 +7,8 @@ import androidx.room.PrimaryKey internal data class Request( @PrimaryKey(autoGenerate = true) val id: Long = 0, val type: ApiType, - val httpMethod: HttpMethod?, val path: String, - val body: String? + val httpMethod: HttpMethod? = null, + val queryParameters: List = emptyList(), + val body: String? = null ) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt index 02b3d74d41b5..e1368bed4f57 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/models/Response.kt @@ -17,5 +17,5 @@ import androidx.room.PrimaryKey internal data class Response( @PrimaryKey val endpointId: Long = 0, val statusCode: Int, - val body: String?, + val body: String? = null ) From c58a989ae421c50adfc732b386d326f7eb57cf64 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 14:53:45 +0100 Subject: [PATCH 57/68] Filter requests using query parameters --- .../android/apifaker/EndpointProcessor.kt | 12 +++++++++++- .../woocommerce/android/apifaker/db/EndpointDao.kt | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 254c3d5ebbe1..a0938986d413 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -3,6 +3,7 @@ package com.woocommerce.android.apifaker import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod +import com.woocommerce.android.apifaker.models.QueryParameter import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.util.JSONObjectProvider import okhttp3.HttpUrl @@ -18,7 +19,6 @@ internal class EndpointProcessor @Inject constructor( private val jsonObjectProvider: JSONObjectProvider ) { fun fakeRequestIfNeeded(request: Request): Response? { - // TODO match against method and query parameters too val endpointData = when { request.url.host == WPCOM_HOST -> request.extractDataFromWPComEndpoint() request.url.encodedPath.startsWith("/wp-json") -> request.extractDataFromWPApiEndpoint() @@ -27,6 +27,8 @@ internal class EndpointProcessor @Inject constructor( return with(endpointData) { endpointDao.queryEndpoint(apiType, endpointData.httpMethod, path.trimEnd('/'), body.orEmpty()) + }.firstOrNull { + request.url.checkQueryParameters(it.request.queryParameters) }?.response?.let { it.copy(body = it.body?.wrapBodyIfNecessary(request.url)) } @@ -87,6 +89,14 @@ internal class EndpointProcessor @Inject constructor( ) } + private fun HttpUrl.checkQueryParameters(queryParameters: List): Boolean { + if (queryParameters.isEmpty()) return true + + return queryParameters.all { queryParameter -> + queryParameter(queryParameter.name) == queryParameter.value + } + } + private fun Request.readBody(): String { val requestBody = body return if (requestBody != null) { diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt index e3efbd08124e..9ddb2d87d353 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/EndpointDao.kt @@ -31,7 +31,7 @@ internal interface EndpointDao { :body LIKE COALESCE(body, '%') """ ) - fun queryEndpoint(type: ApiType, httpMethod: HttpMethod, path: String, body: String): MockedEndpoint? + fun queryEndpoint(type: ApiType, httpMethod: HttpMethod, path: String, body: String): List @Transaction @Query("Select * FROM Request WHERE id = :id") From ddd80eacdcd608a8cebebee5bbbe45875e15aad8 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 18:01:38 +0100 Subject: [PATCH 58/68] Bump db version --- .../com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt index 83742eb6a1c9..f8b5c5ee01fe 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/ApiFakerDatabase.kt @@ -13,7 +13,7 @@ import com.woocommerce.android.apifaker.models.Response Request::class, Response::class ], - version = 1, + version = 2, exportSchema = false ) @TypeConverters( From f2c6ad693692d558caf19ec27f0c4d6f14a88648 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 14:53:56 +0100 Subject: [PATCH 59/68] Add a way to add query parameters in the UI --- .../ui/details/EndpointDetailsScreen.kt | 147 +++++++++++++++++- .../ui/details/EndpointDetailsViewModel.kt | 21 ++- 2 files changed, 165 insertions(+), 3 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index a8a688a34c6b..49ee642007b9 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -4,12 +4,17 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Chip import androidx.compose.material.ChipDefaults @@ -28,6 +33,8 @@ import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -48,6 +55,7 @@ import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod +import com.woocommerce.android.apifaker.models.QueryParameter import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.DropDownMenu @@ -69,6 +77,8 @@ internal fun EndpointDetailsScreen( onApiTypeChanged = viewModel::onApiTypeChanged, onRequestHttpMethodChanged = viewModel::onRequestHttpMethodChanged, onRequestPathChanged = viewModel::onRequestPathChanged, + onQueryParameterAdded = viewModel::onQueryParameterAdded, + onQueryParameterDeleted = viewModel::onQueryParameterDeleted, onRequestBodyChanged = viewModel::onRequestBodyChanged, onResponseStatusCodeChanged = viewModel::onResponseStatusCodeChanged, onResponseBodyChanged = viewModel::onResponseBodyChanged, @@ -83,6 +93,8 @@ private fun EndpointDetailsScreen( onApiTypeChanged: (ApiType) -> Unit = {}, onRequestHttpMethodChanged: (HttpMethod?) -> Unit = {}, onRequestPathChanged: (String) -> Unit = {}, + onQueryParameterAdded: (String, String) -> Unit = { _, _ -> }, + onQueryParameterDeleted: (QueryParameter) -> Unit = {}, onRequestBodyChanged: (String) -> Unit = {}, onResponseStatusCodeChanged: (Int) -> Unit = {}, onResponseBodyChanged: (String) -> Unit = {}, @@ -126,6 +138,8 @@ private fun EndpointDetailsScreen( onApiTypeChanged = onApiTypeChanged, onHttpMethodChanged = onRequestHttpMethodChanged, onPathChanged = onRequestPathChanged, + onQueryParameterAdded = onQueryParameterAdded, + onQueryParameterDeleted = onQueryParameterDeleted, onBodyChanged = onRequestBodyChanged, modifier = Modifier .fillMaxWidth() @@ -154,6 +168,8 @@ private fun RequestDefinitionSection( onApiTypeChanged: (ApiType) -> Unit, onHttpMethodChanged: (HttpMethod?) -> Unit, onPathChanged: (String) -> Unit, + onQueryParameterAdded: (String, String) -> Unit, + onQueryParameterDeleted: (QueryParameter) -> Unit, onBodyChanged: (String) -> Unit, modifier: Modifier = Modifier ) { @@ -184,6 +200,13 @@ private fun RequestDefinitionSection( modifier = Modifier.fillMaxWidth() ) + QueryParametersField( + queryParameters = request.queryParameters, + onQueryParameterAdded = onQueryParameterAdded, + onQueryParameterDeleted = onQueryParameterDeleted, + modifier = Modifier.fillMaxWidth() + ) + RequestBodyField( body = request.body, onBodyChanged = onBodyChanged, @@ -303,7 +326,122 @@ private fun PathField( } Text( text = caption, - style = MaterialTheme.typography.caption + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + ) + } +} + +@Composable +private fun QueryParametersField( + queryParameters: List, + onQueryParameterAdded: (String, String) -> Unit, + onQueryParameterDeleted: (QueryParameter) -> Unit, + modifier: Modifier = Modifier +) { + @Composable + fun AddDialog(onAdd: (String, String) -> Unit, onDismiss: () -> Unit) { + var name by remember { mutableStateOf("") } + var value by remember { mutableStateOf("") } + Dialog(onDismissRequest = onDismiss) { + Column( + Modifier + .background(MaterialTheme.colors.surface, MaterialTheme.shapes.medium) + .padding(16.dp) + ) { + Text(text = "Add Query Parameter (unencoded)") + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + label = { Text(text = "Name") }, + value = name, + onValueChange = { name = it }, + modifier = Modifier.fillMaxWidth() + ) + OutlinedTextField( + label = { Text(text = "Value") }, + value = value, + onValueChange = { value = it }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = onDismiss) { + Text(text = "Cancel") + } + Spacer(modifier = Modifier.width(8.dp)) + Button(onClick = { onAdd(name, value) }) { + Text(text = "Add") + } + } + + } + } + } + + var isAddDialogShown by remember { mutableStateOf(false) } + + Column(modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Query Parameters", + style = MaterialTheme.typography.subtitle1 + ) + IconButton( + onClick = { isAddDialogShown = true }, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add query parameter" + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "A request is matched only if it contains all the parameters listed here", + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) + ) + if (queryParameters.isNotEmpty()) { + queryParameters.forEach { queryParameter -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = "${queryParameter.name} = ${queryParameter.value}", + style = MaterialTheme.typography.caption + ) + IconButton( + onClick = { onQueryParameterDeleted(queryParameter) }, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete query parameter" + ) + } + } + Divider() + } + } + } + + if (isAddDialogShown) { + AddDialog(onAdd = { name, value -> + onQueryParameterAdded(name, value) + isAddDialogShown = false + }, + onDismiss = { isAddDialogShown = false } ) } } @@ -340,7 +478,8 @@ private fun RequestBodyField( } Text( text = caption, - style = MaterialTheme.typography.caption + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) ) } } @@ -480,6 +619,10 @@ private fun EndpointDetailsScreenPreview() { request = Request( type = ApiType.Custom("https://example.com"), httpMethod = HttpMethod.GET, + queryParameters = listOf( + QueryParameter("name", "value"), + QueryParameter("name2", "value2") + ), path = "/wc/v3/products", body = null ), diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index 772749621da7..e10d6e71c1a8 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod +import com.woocommerce.android.apifaker.models.QueryParameter import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.ui.Screen @@ -24,7 +25,7 @@ internal class EndpointDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val endpointDao: EndpointDao ) : ViewModel() { - private val id = savedStateHandle.get(Screen.EndpointDetails.endpointIdArgumentName)!! + private val id = checkNotNull(savedStateHandle.get(Screen.EndpointDetails.endpointIdArgumentName)) var state: UiState by mutableStateOf(defaultEndpoint()) private set @@ -53,6 +54,24 @@ internal class EndpointDetailsViewModel @Inject constructor( } } + fun onQueryParameterAdded(name: String, value: String) { + val queryParameter = QueryParameter(name, value) + withMutableSnapshot { + state = state.copy( + request = state.request.copy( + queryParameters = state.request.queryParameters + queryParameter + ) + ) + } + } + + fun onQueryParameterDeleted(queryParameter: QueryParameter) { + withMutableSnapshot { + state = + state.copy(request = state.request.copy(queryParameters = state.request.queryParameters - queryParameter)) + } + } + fun onRequestBodyChanged(body: String) { withMutableSnapshot { state = state.copy(request = state.request.copy(body = body.ifEmpty { null })) From 67ffb0353412edfeb994374edcb9ff9df9ae634b Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 9 Mar 2023 16:28:24 +0100 Subject: [PATCH 60/68] Some fixes --- .../android/apifaker/ApiFakerInterceptor.kt | 6 +++++- .../android/apifaker/db/QueryParameterConverter.kt | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt index 2b8732113a8b..73f4cad56213 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ApiFakerInterceptor.kt @@ -7,6 +7,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.Protocol import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody +import okhttp3.internal.EMPTY_RESPONSE import javax.inject.Inject private const val ARTIFICIAL_DELAY_MS = 500L @@ -39,7 +40,10 @@ internal class ApiFakerInterceptor @Inject constructor( .message("Fake Response") .code(fakeResponse.statusCode) // TODO check if it's safe to always use JSON as the content type - .body(fakeResponse.body?.toResponseBody("application/json".toMediaType())) + .body( + fakeResponse.body?.toResponseBody("application/json".toMediaType()) + ?: EMPTY_RESPONSE + ) .addHeader("content-type", "application/json") .build() } else { diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt index 726412c0a941..a7fcb0784181 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/db/QueryParameterConverter.kt @@ -5,15 +5,15 @@ import com.woocommerce.android.apifaker.models.QueryParameter internal class QueryParameterConverter { @TypeConverter - fun fromQueryParameters(queryParameters: List?): String? { - return queryParameters?.joinToString("&") { "${it.name}:${it.value}" } + fun fromQueryParameters(queryParameters: List): String { + return queryParameters.joinToString("&") { "${it.name}:${it.value}" } } @TypeConverter - fun toQueryParameters(query: String?): List? { - return query?.split("&")?.map { parts -> + fun toQueryParameters(query: String): List { + return query.takeIf { it.isNotBlank() }?.split("&")?.map { parts -> val (name, value) = parts.split(":") QueryParameter(name, value) - } + } ?: emptyList() } } From a27be3a0dd7fe0995681782a0430b4570fe0b560 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 22:44:13 +0100 Subject: [PATCH 61/68] Handle query parameters extraction for Jetpack tunnel requests --- .../android/apifaker/EndpointProcessor.kt | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index a0938986d413..77bbc694ae82 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -36,7 +36,7 @@ internal class EndpointProcessor @Inject constructor( private fun Request.extractDataFromWPComEndpoint(): EndpointData { val originalBody = readBody() - return if (url.encodedPath.trimEnd('/').matches(Regex(JETPACK_TUNNEL_REGEX))) { + return if (url.isJetpackTunnelRequest) { val (path, method, body) = if (method == "GET") { Triple( url.queryParameter("path")!!.substringBefore("&"), @@ -89,11 +89,22 @@ internal class EndpointProcessor @Inject constructor( ) } - private fun HttpUrl.checkQueryParameters(queryParameters: List): Boolean { - if (queryParameters.isEmpty()) return true + private fun HttpUrl.checkQueryParameters(mockedQueryParameters: List): Boolean { + if (mockedQueryParameters.isEmpty()) return true - return queryParameters.all { queryParameter -> - queryParameter(queryParameter.name) == queryParameter.value + val requestQueryParameters = if (isJetpackTunnelRequest) { + queryParameter("query")?.let { + val json = jsonObjectProvider.parseString(it) + json.keys().asSequence().map { key -> + key to json.getString(key) + }.toMap() + } ?: emptyMap() + } else { + queryParameterNames.associateWith { queryParameter(it) } + } + + return mockedQueryParameters.all { queryParameter -> + requestQueryParameters[queryParameter.name] == queryParameter.value } } @@ -110,8 +121,7 @@ internal class EndpointProcessor @Inject constructor( } private fun String.wrapBodyIfNecessary(url: HttpUrl): String { - return if (url.host == WPCOM_HOST && - url.encodedPath.trimEnd('/').matches(Regex(JETPACK_TUNNEL_REGEX)) && + return if (url.isJetpackTunnelRequest && !startsWith("{\"data\":") ) { "{\"data\": $this}" @@ -123,6 +133,9 @@ internal class EndpointProcessor @Inject constructor( private val Request.httpMethod get() = HttpMethod.valueOf(this.method.uppercase()) + private val HttpUrl.isJetpackTunnelRequest + get() = host == WPCOM_HOST && encodedPath.trimEnd('/').matches(Regex(JETPACK_TUNNEL_REGEX)) + private data class EndpointData( val apiType: ApiType, val path: String, From c41e8204924de9dc0ef11fb8c9531da53fbce2f0 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 4 Dec 2024 23:06:47 +0100 Subject: [PATCH 62/68] Fix detekt issues --- .../apifaker/ui/details/EndpointDetailsScreen.kt | 10 +++++----- .../apifaker/ui/details/EndpointDetailsViewModel.kt | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt index 49ee642007b9..3db6534389a6 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsScreen.kt @@ -376,7 +376,6 @@ private fun QueryParametersField( Text(text = "Add") } } - } } } @@ -437,10 +436,11 @@ private fun QueryParametersField( } if (isAddDialogShown) { - AddDialog(onAdd = { name, value -> - onQueryParameterAdded(name, value) - isAddDialogShown = false - }, + AddDialog( + onAdd = { name, value -> + onQueryParameterAdded(name, value) + isAddDialogShown = false + }, onDismiss = { isAddDialogShown = false } ) } diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt index e10d6e71c1a8..ddb2b041e5d1 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/details/EndpointDetailsViewModel.kt @@ -67,8 +67,11 @@ internal class EndpointDetailsViewModel @Inject constructor( fun onQueryParameterDeleted(queryParameter: QueryParameter) { withMutableSnapshot { - state = - state.copy(request = state.request.copy(queryParameters = state.request.queryParameters - queryParameter)) + state = state.copy( + request = state.request.copy( + queryParameters = state.request.queryParameters - queryParameter + ) + ) } } From f0903058112d447330e80845b56f728287ddde93 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 10 Mar 2023 10:55:35 +0100 Subject: [PATCH 63/68] Log a warning message if multiple endpoints are matched --- .../woocommerce/android/apifaker/EndpointProcessor.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 77bbc694ae82..6633ade6e7a3 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.apifaker +import android.util.Log import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod @@ -27,6 +28,15 @@ internal class EndpointProcessor @Inject constructor( return with(endpointData) { endpointDao.queryEndpoint(apiType, endpointData.httpMethod, path.trimEnd('/'), body.orEmpty()) + }.also { + if (it.size > 1) { + Log.w( + LOG_TAG, + "More than one endpoint matched the request: $request, " + + "the endpoints matched are\n$it\n" + + "The first one will be used." + ) + } }.firstOrNull { request.url.checkQueryParameters(it.request.queryParameters) }?.response?.let { From a85c17cd17fb0b0e31b9cb8d3ebbeee60a52cf74 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 5 Dec 2024 21:06:17 +0100 Subject: [PATCH 64/68] Add more tests --- .../android/apifaker/EndpointProcessor.kt | 2 +- .../android/apifaker/EndpointProcessorTest.kt | 107 ++++++++++++++++-- 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index 6633ade6e7a3..b75011c83698 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -27,7 +27,7 @@ internal class EndpointProcessor @Inject constructor( } return with(endpointData) { - endpointDao.queryEndpoint(apiType, endpointData.httpMethod, path.trimEnd('/'), body.orEmpty()) + endpointDao.queryEndpoint(apiType, httpMethod, path.trimEnd('/'), body.orEmpty()) }.also { if (it.size > 1) { Log.w( diff --git a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt index ae84ddc641b7..35d7c6e335d1 100644 --- a/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt +++ b/libs/apifaker/src/test/java/com/woocommerce/android/apifaker/EndpointProcessorTest.kt @@ -4,9 +4,10 @@ import com.woocommerce.android.apifaker.db.EndpointDao import com.woocommerce.android.apifaker.models.ApiType import com.woocommerce.android.apifaker.models.HttpMethod import com.woocommerce.android.apifaker.models.MockedEndpoint +import com.woocommerce.android.apifaker.models.QueryParameter +import com.woocommerce.android.apifaker.models.Request import com.woocommerce.android.apifaker.models.Response import com.woocommerce.android.apifaker.util.JSONObjectProvider -import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONObject import org.junit.Test @@ -14,6 +15,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import okhttp3.Request as OkHttpRequest class EndpointProcessorTest { private val endpointDaoMock = mock() @@ -25,7 +27,7 @@ class EndpointProcessorTest { @Test fun `when processing a GET WPCom endpoint, then extract data correctly`() { - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("GET", null) .url("https://public-api.wordpress.com/rest/v1.1/me?param=value") .build() @@ -43,7 +45,7 @@ class EndpointProcessorTest { @Test fun `when processing a POST WPCom endpoint, then extract data correctly`() { val body = "Test Body" - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("POST", body.toRequestBody()) .url("https://public-api.wordpress.com/rest/v1.1/me?param=value") .build() @@ -60,7 +62,7 @@ class EndpointProcessorTest { @Test fun `when processing a GET Jetpack Tunnel endpoint, then extract data correctly`() { - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("GET", null) .url( "https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/" + @@ -91,7 +93,7 @@ class EndpointProcessorTest { } whenever(jsonObjectProvider.parseString(body)).thenReturn(jsonObject) - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("POST", body.toRequestBody()) .url("https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api") .build() @@ -108,7 +110,7 @@ class EndpointProcessorTest { @Test fun `when processing a GET WPApi endpoint, then extract data correctly`() { - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("GET", null) .url("https://test-site.com/wp-json/wc/v3/products?param=value") .build() @@ -126,7 +128,7 @@ class EndpointProcessorTest { @Test fun `when processing a POST WPApi endpoint, then extract data correctly`() { val body = "Test Body" - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("POST", body.toRequestBody()) .url("https://test-site.com/wp-json/wc/v3/products") .build() @@ -143,7 +145,7 @@ class EndpointProcessorTest { @Test fun `when processing a GET Custom endpoint, then extract data correctly`() { - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("GET", null) .url("https://test-site.com/an/endpoint?param=value") .build() @@ -161,7 +163,7 @@ class EndpointProcessorTest { @Test fun `when processing a POST Custom endpoint, then extract data correctly`() { val body = "Test Body" - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method("POST", body.toRequestBody()) .url("https://test-site.com/an/endpoint") .build() @@ -179,7 +181,7 @@ class EndpointProcessorTest { @Test fun `when processing a GET jetpack tunnel endpoint, then wrap body if necessary`() { val mockEndpoint = MockedEndpoint( - request = com.woocommerce.android.apifaker.models.Request( + request = Request( id = 0, type = ApiType.WPApi, httpMethod = HttpMethod.GET, @@ -199,9 +201,9 @@ class EndpointProcessorTest { path = mockEndpoint.request.path, body = mockEndpoint.request.body.orEmpty() ) - ).thenReturn(mockEndpoint) + ).thenReturn(listOf(mockEndpoint)) - val request = Request.Builder() + val request = OkHttpRequest.Builder() .method(mockEndpoint.request.httpMethod.name, null) .url( "https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/" + @@ -212,4 +214,85 @@ class EndpointProcessorTest { val response = endpointProcessor.fakeRequestIfNeeded(request) assert(response?.body == "{\"data\": {\"key\":\"value\"}}") } + + @Test + fun `when processing a GET jetpack tunnel endpoint with query parameters, then check query parameters`() { + val mockEndpoint = MockedEndpoint( + request = Request( + id = 0, + type = ApiType.WPApi, + httpMethod = HttpMethod.GET, + path = "/wc/v3/products", + queryParameters = listOf( + QueryParameter("param", "value") + ), + body = null + ), + response = Response( + endpointId = 0, + statusCode = 200 + ) + ) + whenever( + endpointDaoMock.queryEndpoint( + type = mockEndpoint.request.type, + httpMethod = mockEndpoint.request.httpMethod!!, + path = mockEndpoint.request.path, + body = mockEndpoint.request.body.orEmpty() + ) + ).thenReturn(listOf(mockEndpoint)) + val jsonObject = mock { + on { keys() } doReturn listOf("param").iterator() + on { getString("param") } doReturn "value" + } + whenever(jsonObjectProvider.parseString("{\"param\":\"value\"}")).thenReturn(jsonObject) + + val request = OkHttpRequest.Builder() + .method(mockEndpoint.request.httpMethod.name, null) + .url( + "https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/161477129/rest-api/" + + "?path=${mockEndpoint.request.path}&_method=${mockEndpoint.request.httpMethod.name}" + + "&query={\"param\":\"value\"}" + ) + .build() + + val response = endpointProcessor.fakeRequestIfNeeded(request) + assert(response?.statusCode == 200) + } + + @Test + fun `when processing a regular endpoint with query parameters, then check query parameters`() { + val mockEndpoint = MockedEndpoint( + request = Request( + id = 0, + type = ApiType.WPApi, + httpMethod = HttpMethod.GET, + path = "/wc/v3/products", + queryParameters = listOf( + QueryParameter("param", "value") + ), + body = null + ), + response = Response( + endpointId = 0, + statusCode = 200 + ) + ) + whenever( + endpointDaoMock.queryEndpoint( + type = mockEndpoint.request.type, + httpMethod = mockEndpoint.request.httpMethod!!, + path = mockEndpoint.request.path, + body = mockEndpoint.request.body.orEmpty() + ) + ).thenReturn(listOf(mockEndpoint)) + + val request = OkHttpRequest.Builder() + .method(mockEndpoint.request.httpMethod.name, null) + .url("https://test-site.com/wp-json/wc/v3/products?param=value") + .build() + + val response = endpointProcessor.fakeRequestIfNeeded(request) + assert(response?.statusCode == 200) + } } From 9375f2d66b3e93f9706516fda18d5dc44953a2eb Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 10 Dec 2024 08:37:00 +0100 Subject: [PATCH 65/68] Make DB flow collection lifecycle aware --- gradle/libs.versions.toml | 1 + libs/apifaker/build.gradle | 1 + .../com/woocommerce/android/apifaker/ui/home/HomeScreen.kt | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 24214f404fa2..e1514ddab761 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -132,6 +132,7 @@ androidx-hilt-navigation-fragment = { group = "androidx.hilt", name = "hilt-navi androidx-hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "androidx-hilt" } androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-savedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } androidx-navigation-common = { group = "androidx.navigation", name = "navigation-common", version.ref = "androidx-navigation" } diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle index 12c0b345f73b..b08f5bba41dd 100644 --- a/libs/apifaker/build.gradle +++ b/libs/apifaker/build.gradle @@ -52,6 +52,7 @@ android { dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.material.main) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index aff58c7b0c03..0a1340d46d04 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.woocommerce.android.apifaker.models.ApiType @@ -46,7 +47,7 @@ internal fun HomeScreen( onExit: () -> Unit ) { HomeScreen( - endpoints = viewModel.endpoints.collectAsState().value, + endpoints = viewModel.endpoints.collectAsStateWithLifecycle().value, isEnabled = viewModel.isEnabled.collectAsState(initial = false).value, navController = navController, onMockingToggleChanged = viewModel::onMockingToggleChanged, From d4b929ee8811dfada1ab7f2686ff4fcc050a03e7 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 11 Dec 2024 11:58:05 +0100 Subject: [PATCH 66/68] Fix UI with long endpoint paths --- .../android/apifaker/ui/home/HomeScreen.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt index 2c6e81a5b676..0a7f89ac783d 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/ui/home/HomeScreen.kt @@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -168,10 +166,14 @@ private fun EndpointItem( elevation = 4.dp ) { Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.weight(1f) + ) { Text( text = when (endpoint.request.type) { ApiType.WPApi -> "WordPress API" @@ -187,11 +189,6 @@ private fun EndpointItem( style = MaterialTheme.typography.body1 ) } - Spacer( - modifier = Modifier - .weight(1f) - .defaultMinSize(minWidth = 16.dp) - ) Text( text = endpoint.response.statusCode.toString(), style = MaterialTheme.typography.subtitle1 From ad5f00fb9b4796e0ea9baeedcb5b7f3a08529052 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Tue, 17 Dec 2024 11:24:24 +0100 Subject: [PATCH 67/68] Improve logic of warning log This makes sure the log is not printed when the matched endpoints use different query parameters. --- .../com/woocommerce/android/apifaker/EndpointProcessor.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt index b75011c83698..b7090206c6e2 100644 --- a/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt +++ b/libs/apifaker/src/main/java/com/woocommerce/android/apifaker/EndpointProcessor.kt @@ -28,6 +28,8 @@ internal class EndpointProcessor @Inject constructor( return with(endpointData) { endpointDao.queryEndpoint(apiType, httpMethod, path.trimEnd('/'), body.orEmpty()) + }.filter { + request.url.checkQueryParameters(it.request.queryParameters) }.also { if (it.size > 1) { Log.w( @@ -37,9 +39,7 @@ internal class EndpointProcessor @Inject constructor( "The first one will be used." ) } - }.firstOrNull { - request.url.checkQueryParameters(it.request.queryParameters) - }?.response?.let { + }.firstOrNull()?.response?.let { it.copy(body = it.body?.wrapBodyIfNecessary(request.url)) } } From 1f7edf5c2de2424545e61710b3b5dc7dc32e52bc Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 19 Dec 2024 09:20:54 +0100 Subject: [PATCH 68/68] Add Compose icons dependency to the apiFaker lib --- libs/apifaker/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/apifaker/build.gradle b/libs/apifaker/build.gradle index 92652b2755fa..db4e680480ad 100644 --- a/libs/apifaker/build.gradle +++ b/libs/apifaker/build.gradle @@ -59,6 +59,7 @@ dependencies { implementation(libs.androidx.compose.material.main) implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material.icons.extended) debugImplementation(libs.androidx.compose.ui.tooling.main) implementation("${gradle.ext.fluxCBinaryPath}:${libs.versions.wordpress.fluxc.get()}") {