Repository: apollographql/apollo-kotlin
Pull request #4806 (2023-03-27 21:51:11)

Allow custom SqlDriver
Implementation for discussion at https://github.com/apollographql/apollo-kotlin/issues/4504

Publicly exposes `SqlNormalizedCacheFactory` constructors which take a `SqlDriver` argument.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy request for *apollo-android-docs* pending review.
[Visit the deploys page to approve it](https://app.netlify.com/sites/apollo-android-docs/deploys)

|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 8cf3f85e3574f44141c3e7871f3fbf2078fdcf5a |
apollo-cla: @hbmartin: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/

Patch:
diff --git a/gradle/libraries.toml b/gradle/libraries.toml
index 9383182298..599e57660c 100644
--- a/gradle/libraries.toml
+++ b/gradle/libraries.toml
@@ -132,6 +132,7 @@ sqldelight-android = { group = "com.squareup.sqldelight", name = "android-driver
 sqldelight-jvm = { group = "com.squareup.sqldelight", name = "sqlite-driver", version.ref = "sqldelight" }
 sqldelight-native = { group = "com.squareup.sqldelight", name = "native-driver", version.ref = "sqldelight" }
 sqldelight-plugin = { group = "com.squareup.sqldelight", name = "gradle-plugin", version.ref = "sqldelight" }
+sqldelight-runtime = { group = "com.squareup.sqldelight", name = "runtime", version.ref = "sqldelight" }
 truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
 turbine = { group = "app.cash.turbine", name = "turbine", version = "0.7.0" }
 uuid = { group = "com.benasher44", name = "uuid", version = "0.3.1" }
diff --git a/libraries/apollo-api/build.gradle.kts b/libraries/apollo-api/build.gradle.kts
index 19feea8444..726354941d 100644
--- a/libraries/apollo-api/build.gradle.kts
+++ b/libraries/apollo-api/build.gradle.kts
@@ -17,6 +17,13 @@ kotlin {
         api(project(":apollo-annotations"))
       }
     }
+
+    findByName("commonTest")?.apply {
+      dependencies {
+        //implementation(golatac.lib("kotlin.test.junit"))
+        implementation(golatac.lib("kotlin.test"))
+      }
+    }
   }
 }
 
diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/internal/UrlEncode.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/internal/UrlEncode.kt
index 7e43d2ad12..031d32e622 100644
--- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/internal/UrlEncode.kt
+++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/internal/UrlEncode.kt
@@ -3,7 +3,7 @@ package com.apollographql.apollo3.api.http.internal
 import kotlin.native.concurrent.SharedImmutable
 
 @SharedImmutable
-private val RESERVED_CHARS = "!#\$&'\"()*+,/:;=?@[]{} "
+private val RESERVED_CHARS = "!#\$&'\"()*+,/:;=?@[]{}% "
 
 /**
  * A very simple urlEncode.
diff --git a/libraries/apollo-compose-paging-support/src/main/java/com/apollographql/apollo3/compose/paging/ApolloPagingSource.kt b/libraries/apollo-compose-paging-support/src/main/java/com/apollographql/apollo3/compose/paging/ApolloPagingSource.kt
index d24f6eedad..3f07d75d37 100644
--- a/libraries/apollo-compose-paging-support/src/main/java/com/apollographql/apollo3/compose/paging/ApolloPagingSource.kt
+++ b/libraries/apollo-compose-paging-support/src/main/java/com/apollographql/apollo3/compose/paging/ApolloPagingSource.kt
@@ -21,34 +21,34 @@ class ApolloPagingSource<Data : Operation.Data, Value : Any>(
      * The response will be null for the initial/refresh call.
      * Return null if there is no next page.
      */
-    private val appendCall: (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,
+    private val appendCall: suspend (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,
 
     /**
      * Count of items after the loaded data, given the response for the current page and the count of loaded items so far.
      * Used to display placeholders.
      * Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
      */
-    private val itemsAfter: (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> LoadResult.Page.COUNT_UNDEFINED },
+    private val itemsAfter: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> LoadResult.Page.COUNT_UNDEFINED },
 
     /**
      * The call to fetch the previous page, given the response for the current page.
      * Return null if there is no previous page.
      * Can be null if prepend is not supported.
      */
-    private val prependCall: ((response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,
+    private val prependCall: (suspend (response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,
 
     /**
      * Count of items before the loaded data, given the response for the current page and the count of loaded items so far.
      * Used to display placeholders.
      * Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
      */
-    private val itemsBefore: (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> LoadResult.Page.COUNT_UNDEFINED },
+    private val itemsBefore: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> LoadResult.Page.COUNT_UNDEFINED },
 
     /**
      * Extract the list of items from a response.
      * Return [Result.failure] if the response cannot be used (e.g. it contains GraphQL errors).
      */
-    private val getItems: (response: ApolloResponse<Data>) -> Result<List<Value>>,
+    private val getItems: suspend (response: ApolloResponse<Data>) -> Result<List<Value>>,
 ) : PagingSource<ApolloCall<Data>, Value>() {
   private var loadedItemsCount = 0
 
@@ -104,34 +104,34 @@ fun <Data : Operation.Data, Value : Any> Pager(
      * The response will be null for the initial/refresh call.
      * Return null if there is no next page.
      */
-    appendCall: (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,
+    appendCall: suspend (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,
 
     /**
      * Count of items after the loaded data, given the response for the current page and the count of loaded items so far.
      * Used to display placeholders.
      * Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
      */
-    itemsAfter: (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
+    itemsAfter: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
 
     /**
      * The call to fetch the previous page, given the response for the current page.
      * Return null if there is no previous page.
      * Can be null if prepend is not supported.
      */
-    prependCall: ((response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,
+    prependCall: (suspend (response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,
 
     /**
      * Count of items before the loaded data, given the response for the current page and the count of loaded items so far.
      * Used to display placeholders.
      * Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
      */
-    itemsBefore: (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
+    itemsBefore: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
 
     /**
      * Extract the list of items from a response.
      * Return [Result.failure] if the response cannot be used (e.g. it contains GraphQL errors).
      */
-    getItems: (response: ApolloResponse<Data>) -> Result<List<Value>>,
+    getItems: suspend (response: ApolloResponse<Data>) -> Result<List<Value>>,
 ): Pager<ApolloCall<Data>, Value> {
   return Pager(
       config = config,
@@ -161,34 +161,34 @@ fun <Data : Operation.Data, Value : Any> rememberAndCollectPager(
      * The response will be null for the initial/refresh call.
      * Return null if there is no next page.
      */
-    appendCall: (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,
+    appendCall: suspend (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,
 
     /**
      * Count of items after the loaded data, given the response for the current page and the count of loaded items so far.
      * Used to display placeholders.
      * Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
      */
-    itemsAfter: (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
+    itemsAfter: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
 
     /**
      * The call to fetch the previous page, given the response for the current page.
      * Can be null if prepend is not supported.
      * Return null if there is no previous page.
      */
-    prependCall: ((response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,
+    prependCall: (suspend (response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,
 
     /**
      * Count of items before the loaded data, given the response for the current page and the count of loaded items so far.
      * Used to display placeholders.
      * Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
      */
-    itemsBefore: (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
+    itemsBefore: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },
 
     /**
      * Extract the list of items from a response.
      * Return [Result.failure] if the response cannot be used (e.g. it contains GraphQL errors).
      */
-    getItems: (response: ApolloResponse<Data>) -> Result<List<Value>>,
+    getItems: suspend (response: ApolloResponse<Data>) -> Result<List<Value>>,
 ): LazyPagingItems<Value> {
   val pager = remember {
     Pager(
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/build.gradle.kts b/libraries/apollo-normalized-cache-sqlite-incubating/build.gradle.kts
index 15dbd2bcc3..196ab84702 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/build.gradle.kts
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/build.gradle.kts
@@ -44,6 +44,7 @@ kotlin {
         api(project(":apollo-api"))
         api(project(":apollo-normalized-cache-api-incubating"))
         api(project(":apollo-normalized-cache-incubating"))
+        api(golatac.lib("sqldelight.runtime"))
       }
     }
 
@@ -88,7 +89,7 @@ kotlin {
 
 configure<com.android.build.gradle.LibraryExtension> {
   compileSdk = golatac.version("android.sdkversion.compile").toInt()
-  namespace ="com.apollographql.apollo3.cache.normalized.sql"
+  namespace = "com.apollographql.apollo3.cache.normalized.sql"
 
   defaultConfig {
     minSdk = golatac.version("android.sdkversion.min").toInt()
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/src/androidMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt b/libraries/apollo-normalized-cache-sqlite-incubating/src/androidMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
index 419d3962cf..a271edbfae 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/src/androidMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/src/androidMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
@@ -10,7 +10,7 @@ import com.apollographql.apollo3.cache.normalized.sql.internal.getSchema
 import com.squareup.sqldelight.android.AndroidSqliteDriver
 import com.squareup.sqldelight.db.SqlDriver
 
-actual class SqlNormalizedCacheFactory internal constructor(
+actual class SqlNormalizedCacheFactory actual constructor(
     private val driver: SqlDriver,
     private val withDates: Boolean,
 ) : NormalizedCacheFactory() {
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/src/appleMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt b/libraries/apollo-normalized-cache-sqlite-incubating/src/appleMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
index 9b644b4a0c..4ef43abe61 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/src/appleMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/src/appleMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
@@ -7,7 +7,7 @@ import com.apollographql.apollo3.cache.normalized.sql.internal.getSchema
 import com.squareup.sqldelight.db.SqlDriver
 import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
 
-actual class SqlNormalizedCacheFactory internal constructor(
+actual class SqlNormalizedCacheFactory actual constructor(
     private val driver: SqlDriver,
     private val withDates: Boolean,
 ) : NormalizedCacheFactory() {
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
index a7c534f881..c02899cf30 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactory.kt
@@ -1,6 +1,7 @@
 package com.apollographql.apollo3.cache.normalized.sql
 
 import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory
+import com.squareup.sqldelight.db.SqlDriver
 
 /**
  * Creates a new [NormalizedCacheFactory] that uses a persistent cache based on Sqlite
@@ -16,5 +17,7 @@ import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory
  * Once a database is created, this parameter cannot change
  * Default: false
  */
-expect class SqlNormalizedCacheFactory(name: String? = "apollo.db", withDates: Boolean = false) : NormalizedCacheFactory
+expect class SqlNormalizedCacheFactory(name: String? = "apollo.db", withDates: Boolean = false) : NormalizedCacheFactory {
+  constructor(driver: SqlDriver, withDates: Boolean = false)
+}
 
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/src/jvmMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactoryJvm.kt b/libraries/apollo-normalized-cache-sqlite-incubating/src/jvmMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactoryJvm.kt
index 4180cb23f2..0640a860f6 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/src/jvmMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactoryJvm.kt
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/src/jvmMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheFactoryJvm.kt
@@ -8,7 +8,7 @@ import com.squareup.sqldelight.db.SqlDriver
 import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
 import java.util.Properties
 
-actual class SqlNormalizedCacheFactory internal constructor(
+actual class SqlNormalizedCacheFactory actual constructor(
     private val driver: SqlDriver,
     private val withDates: Boolean,
 ) : NormalizedCacheFactory() {
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/LoggingInterceptor.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/LoggingInterceptor.kt
index 0284842831..8a7fa19af1 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/LoggingInterceptor.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/LoggingInterceptor.kt
@@ -1,17 +1,29 @@
 package com.apollographql.apollo3.network.http
 
 import com.apollographql.apollo3.api.http.ByteStringHttpBody
+import com.apollographql.apollo3.api.http.HttpBody
 import com.apollographql.apollo3.api.http.HttpRequest
 import com.apollographql.apollo3.api.http.HttpResponse
+import com.apollographql.apollo3.api.http.valueOf
 import com.apollographql.apollo3.network.http.LoggingInterceptor.Level
 import okio.Buffer
+import okio.BufferedSink
+import okio.BufferedSource
+import okio.Sink
+import okio.Source
+import okio.Timeout
+import okio.buffer
+import okio.use
 import kotlin.jvm.JvmOverloads
 
 /**
  * An interceptor that logs requests and responses.
  *
- * @param [level] the level of logging. Caution: when uploading files, setting this to [Level.BODY] will cause the files to
+ * @param level the level of logging. Caution: when uploading files, setting this to [Level.BODY] will cause the files to
  * be fully loaded into memory, which may cause OutOfMemoryErrors.
+ *
+ * @param log a callback that gets called with a line of log. Contains headers and/or body. No attempt is made at detecting
+ * binary bodies. Do use [Level.BODY] with binary content
  */
 class LoggingInterceptor(
     private val level: Level,
@@ -74,6 +86,35 @@ class LoggingInterceptor(
     BODY,
   }
 
+  private fun BufferedSource.intercept(): Source {
+    return object: Source {
+      private val buffer = Buffer()
+
+      override fun close() {
+        this@intercept.close()
+      }
+
+      override fun read(sink: Buffer, byteCount: Long): Long {
+        val tmp = Buffer()
+        val read = this@intercept.read(tmp, byteCount)
+        buffer.writeAll(tmp.peek())
+        while (true) {
+          val next = buffer.indexOf('\n'.code.toByte())
+          if (next == -1L) {
+            break
+          }
+          log(buffer.readUtf8Line()!!)
+        }
+        sink.writeAll(tmp)
+        return read
+      }
+
+      override fun timeout(): Timeout {
+        return Timeout.NONE
+      }
+    }
+  }
+
   override suspend fun intercept(
       request: HttpRequest,
       chain: HttpInterceptorChain,
@@ -122,11 +163,8 @@ class LoggingInterceptor(
     return if (!logBody || responseBody == null) {
       httpResponse
     } else {
-      val bodyByteString = responseBody.readByteString()
-      log(bodyByteString.utf8())
-      @Suppress("DEPRECATION")
       HttpResponse.Builder(statusCode = httpResponse.statusCode)
-          .body(bodyByteString)
+          .body(responseBody.intercept().buffer())
           .addHeaders(httpResponse.headers)
           .build()
     }


Test patch:
diff --git a/libraries/apollo-api/src/jvmTest/kotlin/UrlEncodeTest.kt b/libraries/apollo-api/src/jvmTest/kotlin/UrlEncodeTest.kt
new file mode 100644
index 0000000000..933b36b46e
--- /dev/null
+++ b/libraries/apollo-api/src/jvmTest/kotlin/UrlEncodeTest.kt
@@ -0,0 +1,18 @@
+import com.apollographql.apollo3.api.http.internal.urlEncode
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class UrlEncodeTest {
+
+  @Test
+  fun test() {
+    listOf(
+        "%" to "%25",
+        " " to "%20",
+        "{}" to "%7B%7D",
+        "e30=" to "e30%3D", // {} base64 encoded
+    ).forEach {
+      assertEquals(it.second, it.first.urlEncode())
+    }
+  }
+}
\ No newline at end of file
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/OpenDbTest.kt b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/OpenDbTest.kt
index d3b82ab82d..188476a5b6 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/OpenDbTest.kt
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/OpenDbTest.kt
@@ -1,8 +1,6 @@
 package com.apollographql.apollo3.cache.normalized.sql
 
-import com.apollographql.apollo3.mpp.Platform
 import com.apollographql.apollo3.testing.HostFileSystem
-import okio.FileSystem
 import okio.Path.Companion.toPath
 import kotlin.test.Test
 import kotlin.test.assertTrue
diff --git a/tests/integration-tests/src/commonTest/kotlin/test/HttpGetTest.kt b/tests/integration-tests/src/commonTest/kotlin/test/HttpGetTest.kt
index ca2841f939..2c85537eac 100644
--- a/tests/integration-tests/src/commonTest/kotlin/test/HttpGetTest.kt
+++ b/tests/integration-tests/src/commonTest/kotlin/test/HttpGetTest.kt
@@ -2,15 +2,17 @@ package test
 
 import com.apollographql.apollo3.api.http.HttpMethod
 import com.apollographql.apollo3.integration.normalizer.HeroAndFriendsNamesQuery
+import com.apollographql.apollo3.integration.normalizer.SearchHeroQuery
 import com.apollographql.apollo3.integration.normalizer.type.Episode
 import com.apollographql.apollo3.mockserver.enqueue
+import com.apollographql.apollo3.testing.enqueueData
 import com.apollographql.apollo3.testing.mockServerTest
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
 class HttpGetTest {
   @Test
-  fun gzipTest() = mockServerTest {
+  fun getTest() = mockServerTest {
     mockServer.enqueue("""
       {
         "data": {
@@ -36,4 +38,16 @@ class HttpGetTest {
         mockServer.takeRequest().path
     )
   }
+  @Test
+  fun encodeReservedCharactersTest() = mockServerTest {
+    // Response not needed, just testing generated url
+    mockServer.enqueueData(data = emptyMap())
+    apolloClient.query(SearchHeroQuery("!#$&'()*+,/:;=?@[]{}% "))
+            .httpMethod(HttpMethod.Get)
+            .execute()
+    assertEquals(
+            "/?operationName=SearchHero&variables=%7B%22text%22%3A%22%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D%7B%7D%25%20%22%7D&query=query%20SearchHero%28%24text%3A%20String%29%20%7B%20search%28text%3A%20%24text%29%20%7B%20__typename%20...%20on%20Character%20%7B%20__typename%20name%20...%20on%20Human%20%7B%20homePlanet%20%7D%20...%20on%20Droid%20%7B%20primaryFunction%20%7D%20%7D%20%7D%20%7D",
+            mockServer.takeRequest().path
+    )
+  }
 }
diff --git a/tests/integration-tests/src/main/graphql/com/apollographql/apollo3/integration/normalizer/SearchHero.graphql b/tests/integration-tests/src/main/graphql/com/apollographql/apollo3/integration/normalizer/SearchHero.graphql
new file mode 100644
index 0000000000..4106b95066
--- /dev/null
+++ b/tests/integration-tests/src/main/graphql/com/apollographql/apollo3/integration/normalizer/SearchHero.graphql
@@ -0,0 +1,15 @@
+query SearchHero($text: String) {
+    search(text: $text) {
+        __typename
+        ... on Character {
+            __typename
+            name
+            ... on Human {
+                homePlanet
+            }
+            ... on Droid {
+                primaryFunction
+            }
+        }
+    }
+}