diff --git a/FloconAndroid/app/build.gradle.kts b/FloconAndroid/app/build.gradle.kts index 8f6b7a65..62015058 100644 --- a/FloconAndroid/app/build.gradle.kts +++ b/FloconAndroid/app/build.gradle.kts @@ -52,8 +52,8 @@ dependencies { implementation("io.github.openflocon:flocon-grpc-interceptor:$floconVersion") implementation("io.github.openflocon:flocon-okhttp-interceptor:$floconVersion") } else { - //implementation(project(":flocon")) - implementation(project(":flocon-no-op")) + implementation(project(":flocon")) + //implementation(project(":flocon-no-op")) implementation(project(":okhttp-interceptor")) implementation(project(":grpc-interceptor")) } diff --git a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt index 47dcad37..98aeee50 100644 --- a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt +++ b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt @@ -2,7 +2,9 @@ package io.github.openflocon.flocon.plugins.network import io.github.openflocon.flocon.core.FloconPlugin import io.github.openflocon.flocon.plugins.network.model.FloconNetworkRequest +import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse interface FloconNetworkPlugin : FloconPlugin { + val mocks: Collection fun log(call: FloconNetworkRequest) } \ No newline at end of file diff --git a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt index 55a89adf..f4a38615 100644 --- a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt +++ b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt @@ -5,6 +5,7 @@ data class FloconNetworkRequest( val response: Response, val durationMs: Double, val floconNetworkType: String, + val isMocked: Boolean, ) { data class Request( val url: String, diff --git a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/MockNetworkResponse.kt b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/MockNetworkResponse.kt new file mode 100644 index 00000000..eecb98a2 --- /dev/null +++ b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/MockNetworkResponse.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.flocon.plugins.network.model + +import java.util.regex.Pattern + +data class MockNetworkResponse( + val expectation: Expectation, + val response: Response, +) { + data class Expectation( + val urlPattern: String, // a regex + val pattern: Pattern, + val method: String, // can be get, post, put, ... or a wildcard * + ) + + data class Response( + val httpCode: Int, + val body: String, + val mediaType: String, + val delay: Long, + val headers: Map, + ) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt index 8397f16f..867f939f 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt @@ -120,6 +120,14 @@ object Protocol { } } + object Network { + const val Plugin = "network" + + object Method { + const val SetupMocks = "setupMocks" + } + } + object SharedPreferences { const val Plugin = "sharedPreferences" diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/client/FloconClientImpl.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/client/FloconClientImpl.kt index 2dc70250..5146a91f 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/client/FloconClientImpl.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/client/FloconClientImpl.kt @@ -49,7 +49,7 @@ internal class FloconClientImpl( override val tablePlugin = FloconTablePluginImpl(sender = this) override val deeplinksPlugin = FloconDeeplinksPluginImpl(sender = this) override val analyticsPlugin = FloconAnalyticsPluginImpl(sender = this) - override val networkPlugin = FloconNetworkPluginImpl(sender = this) + override val networkPlugin = FloconNetworkPluginImpl(context = appContext, sender = this) private val allPlugins = listOf( databasePlugin, @@ -126,6 +126,13 @@ internal class FloconClientImpl( sender = this@FloconClientImpl, ) } + + Protocol.ToDevice.Network.Plugin -> { + networkPlugin.onMessageReceived( + messageFromServer = messageFromServer, + sender = this@FloconClientImpl, + ) + } } } } diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt index 1728998b..251b99dc 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt @@ -1,15 +1,28 @@ package io.github.openflocon.flocon.plugins.network +import android.content.Context +import io.github.openflocon.flocon.FloconLogger import io.github.openflocon.flocon.Protocol import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.model.FloconMessageFromServer +import io.github.openflocon.flocon.plugins.network.mapper.floconNetworkRequestToJson +import io.github.openflocon.flocon.plugins.network.mapper.parseMockResponses +import io.github.openflocon.flocon.plugins.network.mapper.writeMockResponsesToJson import io.github.openflocon.flocon.plugins.network.model.FloconNetworkRequest -import io.github.openflocon.flocon.plugins.network.model.floconNetworkRequestToJson +import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.concurrent.CopyOnWriteArrayList +import java.util.regex.Pattern class FloconNetworkPluginImpl( + private val context: Context, private var sender: FloconMessageSender, ) : FloconNetworkPlugin { + override val mocks = CopyOnWriteArrayList(loadMocksFromFile()) + override fun log(call: FloconNetworkRequest) { sender.send( plugin = Protocol.FromDevice.Network.Plugin, @@ -22,11 +35,65 @@ class FloconNetworkPluginImpl( messageFromServer: FloconMessageFromServer, sender: FloconMessageSender ) { - // no op + when (messageFromServer.method) { + Protocol.ToDevice.Network.Method.SetupMocks -> { + val setup = parseMockResponses(messageFromServer.body) + mocks.clear() + mocks.addAll(setup) + saveMocksToFile(mocks) + } + } } override fun onConnectedToServer(sender: FloconMessageSender) { // no op } + private fun saveMocksToFile(mocks: CopyOnWriteArrayList) { + try { + val file = File(context.filesDir, "flocon_network_mocks.json") + val jsonString = writeMockResponsesToJson(mocks).toString(2) + FileOutputStream(file).use { + it.write(jsonString.toByteArray()) + } + } catch (t: Throwable) { + FloconLogger.logError("issue in saveMocksToFile", t) + } + } + + private fun loadMocksFromFile(): List { + /* + return listOf( + MockNetworkResponse( + expectation = MockNetworkResponse.Expectation( + method = "*", + urlPattern = ".*todo.*", + pattern = Pattern.compile(".*"), + ), + response = MockNetworkResponse.Response( + httpCode = 201, + mediaType = "application/json", + body = "{ \"florent\" : \"champigny\" }", + delay = 0L, + headers = emptyMap(), + ) + ) + ) + */ + + return try { + val file = File(context.filesDir, "flocon_network_mocks.json") + if (!file.exists()) { + return emptyList() + } + + val jsonString = FileInputStream(file).use { + it.readBytes().toString(Charsets.UTF_8) + } + parseMockResponses(jsonString) + } catch (t: Throwable) { + FloconLogger.logError("issue in loadMocksFromFile", t) + emptyList() + } + } } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt similarity index 84% rename from FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt rename to FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt index 848a37b9..d77f341e 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconHttpRequest.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt @@ -1,5 +1,6 @@ -package io.github.openflocon.flocon.plugins.network.model +package io.github.openflocon.flocon.plugins.network.mapper +import io.github.openflocon.flocon.plugins.network.model.FloconNetworkRequest import org.json.JSONObject @@ -8,6 +9,7 @@ fun floconNetworkRequestToJson(network: FloconNetworkRequest): JSONObject { with(network) { json.put("floconNetworkType", floconNetworkType) + json.put("isMocked", isMocked) json.put("url", request.url) json.put("method", request.method) diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt new file mode 100644 index 00000000..7b826dba --- /dev/null +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/MockResponseToJson.kt @@ -0,0 +1,110 @@ +package io.github.openflocon.flocon.plugins.network.mapper + +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse +import org.json.JSONArray +import org.json.JSONObject +import java.util.regex.Pattern + + +fun parseMockResponses(jsonString: String): List { + val mockResponses = mutableListOf() + try { + val jsonArray = JSONArray(jsonString) + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.getJSONObject(i) + + decodeMockNetworkResponse(jsonObject)?.let { + mockResponses.add(it) + } + } + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "mock network parsing issue", t) + return emptyList() + } + return mockResponses +} + +private fun decodeMockNetworkResponse(jsonObject: JSONObject): MockNetworkResponse? { + return try { + val expectationJson = jsonObject.getJSONObject("expectation") + val urlPattern = expectationJson.getString("urlPattern") + val method = expectationJson.getString("method") + val expectation = MockNetworkResponse.Expectation( + urlPattern = urlPattern, + pattern = Pattern.compile(urlPattern), + method = method, + ) + + val responseJson = jsonObject.getJSONObject("response") + val httpCode = responseJson.getInt("httpCode") + val body = responseJson.getString("body") + val mediaType = responseJson.getString("mediaType") + val delay = responseJson.getLong("delay") + + val headersJson = responseJson.getJSONObject("headers") + val headers = buildMap { + val keys = headersJson.keys() + while (keys.hasNext()) { + val key = keys.next() + put(key = key, value = headersJson.getString(key)) + } + } + + val response = MockNetworkResponse.Response( + httpCode = httpCode, + body = body, + mediaType = mediaType, + delay = delay, + headers = headers + ) + + MockNetworkResponse(expectation, response) + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "mock network parsing issue", t) + null + } +} + + +fun writeMockResponsesToJson(mocks: List): JSONArray { + val jsonArray = JSONArray() + try { + mocks.forEach { mock -> + val jsonObject = encodeMockNetworkResponse(mock) + jsonArray.put(jsonObject) + } + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "mock network writing issue", t) + } + return jsonArray +} + +private fun encodeMockNetworkResponse(mock: MockNetworkResponse): JSONObject { + return try { + val expectationJson = JSONObject().apply { + put("urlPattern", mock.expectation.urlPattern) + put("method", mock.expectation.method) + // L'objet Pattern ne peut pas être sérialisé directement en JSON. + // On le laisse de côté, il sera recréé lors du parsing. + } + + val headersJson = JSONObject(mock.response.headers) + + val responseJson = JSONObject().apply { + put("httpCode", mock.response.httpCode) + put("body", mock.response.body) + put("mediaType", mock.response.mediaType) + put("delay", mock.response.delay) + put("headers", headersJson) + } + + JSONObject().apply { + put("expectation", expectationJson) + put("response", responseJson) + } + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "mock network writing issue", t) + JSONObject() + } +} diff --git a/FloconAndroid/grpc-interceptor/src/main/java/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt b/FloconAndroid/grpc-interceptor/src/main/java/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt index a5929ef2..2b7bfcb4 100644 --- a/FloconAndroid/grpc-interceptor/src/main/java/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt +++ b/FloconAndroid/grpc-interceptor/src/main/java/io/github/openflocon/flocon/grpc/FloconGrpcPlugin.kt @@ -21,6 +21,7 @@ internal class FloconGrpcPlugin() { request = request, response = response, floconNetworkType = "grpc", + isMocked = false, ) FloconApp.instance?.client?.networkPlugin?.log(call) diff --git a/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt index 5647a1b0..487bcf38 100644 --- a/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt +++ b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/OkHttpInterceptor.kt @@ -1,10 +1,16 @@ package io.github.openflocon.flocon.okhttp import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.FloconLogger +import io.github.openflocon.flocon.plugins.network.FloconNetworkPlugin import io.github.openflocon.flocon.plugins.network.model.FloconNetworkRequest import okhttp3.Interceptor import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Protocol +import okhttp3.Request import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import java.io.IOException import java.nio.charset.StandardCharsets @@ -41,7 +47,15 @@ class FloconOkhttpInterceptor() : Interceptor { } val startTime = System.nanoTime() - val response = chain.proceed(request) + + var isMocked = false + val response = tryToMock( + request = request, + floconNetworkPlugin = floconNetworkPlugin, + )?.also { + isMocked = true + } ?: chain.proceed(request) + val endTime = System.nanoTime() val durationMs: Double = (endTime - startTime) / 1e6 @@ -87,7 +101,8 @@ class FloconOkhttpInterceptor() : Interceptor { headers = responseHeadersMap, size = responseSize, grpcStatus = null, - ) + ), + isMocked = isMocked, ) floconNetworkPlugin.log(floconRequest) @@ -97,4 +112,50 @@ class FloconOkhttpInterceptor() : Interceptor { // Just return the original response if you don't modify the body itself. return response } + + private fun tryToMock( + request: Request, + floconNetworkPlugin: FloconNetworkPlugin, + ): Response? { + for (mock in floconNetworkPlugin.mocks) { + val url = request.url.toString() + val method = request.method + + val urlMatches = mock.expectation.pattern.matcher(url).matches() + val methodMatches = mock.expectation.method == "*" || mock.expectation.method.equals( + method, + ignoreCase = true + ) + + if (urlMatches && methodMatches) { + FloconLogger.log("Request $url mocked with HTTP code ${mock.response.httpCode}") + + if (mock.response.delay > 0) { + try { + Thread.sleep(mock.response.delay) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } + } + + val body = mock.response.body.toResponseBody( + mock.response.mediaType.toMediaTypeOrNull() + ) + + return Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .message(getHttpMessage(mock.response.httpCode)) + .code(mock.response.httpCode) + .body(body) + .apply { + mock.response.headers.forEach { (name, value) -> + addHeader(name, value) + } + } + .build() + } + } + return null + } } \ No newline at end of file diff --git a/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/Utils.kt b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/Utils.kt new file mode 100644 index 00000000..333daab2 --- /dev/null +++ b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/Utils.kt @@ -0,0 +1,45 @@ +package io.github.openflocon.flocon.okhttp + +internal fun getHttpMessage(httpCode: Int): String { + return when (httpCode) { + // 1xx Informational + 100 -> "Continue" + 101 -> "Switching Protocols" + 103 -> "Early Hints" + + // 2xx Success + 200 -> "OK" + 201 -> "Created" + 202 -> "Accepted" + 204 -> "No Content" + 206 -> "Partial Content" + + // 3xx Redirection + 300 -> "Multiple Choices" + 301 -> "Moved Permanently" + 302 -> "Found" + 304 -> "Not Modified" + 307 -> "Temporary Redirect" + 308 -> "Permanent Redirect" + + // 4xx Client Error + 400 -> "Bad Request" + 401 -> "Unauthorized" + 403 -> "Forbidden" + 404 -> "Not Found" + 405 -> "Method Not Allowed" + 408 -> "Request Timeout" + 409 -> "Conflict" + 410 -> "Gone" + 429 -> "Too Many Requests" + + // 5xx Server Error + 500 -> "Internal Server Error" + 501 -> "Not Implemented" + 502 -> "Bad Gateway" + 503 -> "Service Unavailable" + 504 -> "Gateway Timeout" + + else -> "Unknown" + } +} \ No newline at end of file