-
Notifications
You must be signed in to change notification settings - Fork 136
[Hack Week] ApiFaker: introduce new module and logic #13052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d64107d
61c9e55
571b1b4
bea598f
e3c4cbb
dccd118
cbf04b6
a9ad0ca
9864ff6
7586064
97f8325
566bb53
cd04882
e637209
1eb669e
9a5c208
124ecf7
4982268
73ba162
3d1c8c9
c8ca8a3
4efe69b
c8d9067
bd0e182
f68afa2
8c8af90
4d1b579
4ebb937
29f734d
82c3c21
c2b1f84
9dd1b5d
9b2d033
a25dfe5
f56d3ac
bedf849
7134c74
b02ce89
72899dc
7387eec
e4ff1be
978c398
2d23d51
dacfd29
d27d000
ea138c3
d4d4775
4f15cd4
52e10d2
fe1c676
876c3d9
33dbf0f
6c5bfe9
7534268
1412386
43b60e2
c58a989
ddd80ea
f2c6ad6
67ffb03
a27be3a
c41e820
f090305
a85c17c
9375f2d
d4b929e
ad5f00f
60ba59a
e02f25c
d209625
933fbbb
b29b2e6
1f7edf5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 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 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 | ||
| 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( | ||
| onExit = { findNavController().navigateUp() } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| 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) | ||
| } | ||
|
|
||
| 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" | ||
|
Comment on lines
+13
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need all these? I see you only add FluxC as dependency (which also makes sense since it's where all the requests happen). But why include
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like this either, but I tried to just keep the same setup as what we have in the other modules, personally I think this code should be in the root build.gradle file, or better in a convention plugin, but since this is not the setup we have yet, I prefer to keep the same setup as the other modules here, as the |
||
| } | ||
| } | ||
|
|
||
| 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(libs.androidx.core.ktx) | ||
| implementation(libs.androidx.appcompat) | ||
| implementation(libs.androidx.navigation.compose) | ||
| implementation(libs.androidx.lifecycle.runtime.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) | ||
| implementation(libs.androidx.compose.material.icons.extended) | ||
| debugImplementation(libs.androidx.compose.ui.tooling.main) | ||
|
|
||
| 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) | ||
|
|
||
| implementation(libs.google.dagger.hilt.android.main) | ||
| implementation(libs.androidx.hilt.navigation.compose) | ||
| ksp(libs.google.dagger.hilt.compiler) | ||
|
|
||
| testImplementation(libs.junit) | ||
| testImplementation(libs.mockito.kotlin) | ||
| testImplementation(libs.mockito.inline) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| </manifest> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.woocommerce.android.apifaker | ||
|
|
||
| import android.content.Context | ||
| import android.content.SharedPreferences | ||
| import com.woocommerce.android.apifaker.db.EndpointDao | ||
| 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.combine | ||
| 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 | ||
| internal class ApiFakerConfig @Inject constructor( | ||
| context: Context, | ||
| 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 = combine( | ||
| prefFlow, | ||
| endpointDao.observeEndpointsCount().map { it == 0 } | ||
| ) { pref, isEmpty -> | ||
| pref && !isEmpty | ||
| }.stateIn(configScope, SharingStarted.Eagerly, false) | ||
|
|
||
| fun setStatus(enabled: Boolean) { | ||
| preferences.edit().putBoolean(PREFERENCE_KEY, enabled).apply() | ||
| } | ||
|
|
||
| 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) } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.woocommerce.android.apifaker | ||
|
|
||
| import android.util.Log | ||
| import okhttp3.Interceptor | ||
| import okhttp3.Interceptor.Chain | ||
| 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 | ||
|
|
||
| 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 { | ||
| 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, "Matched request: ${chain.request().url}:\nSending Mocked Response: $fakeResponse") | ||
| Thread.sleep(ARTIFICIAL_DELAY_MS) | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the body of the faked request is anything the user inputs from the app, wouldn't it make more sense to leave
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good point; but for now I'll keep it like this, my rationale for hardcording
It should work as you expect from the app, the upper layer will try to deserialize the content, and would fail, then return a JSON error, this is what happens when we receive HTML instead of JSON (for example this case: p1723565504566789/1720009283.216679-slack-C6H8C3G23 |
||
| .body( | ||
| fakeResponse.body?.toResponseBody("application/json".toMediaType()) | ||
| ?: EMPTY_RESPONSE | ||
| ) | ||
| .addHeader("content-type", "application/json") | ||
| .build() | ||
| } else { | ||
| chain.proceed(request) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows running unit tests for all modules instead of having to list all of them