Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions firebase-dataconnect/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Unreleased
- [changed] Ignore unknown fields in response data instead of throwing a
`DataConnectOperationException` with message "decoding data from the server's response failed:
An unknown field for index -3"

# 17.0.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,10 @@ mutation createPersonWithPartialFailureInTransaction($id: String!, $name: String
person2: person_insert(data: { id_expr: "uuidV4()", name: $name }) @check(expr: "false", message: "te36b3zkvn")
}

mutation create5People @auth(level: PUBLIC) {
person1: person_upsert(data: { id: "8a218894521f457a9be45a0986494058", name: "Name1" })
person2: person_upsert(data: { id: "464443371f284194be4b2e78c3ef000c", name: "Name2" })
person3: person_upsert(data: { id: "903d83db81754bd29860458f127ef124", name: "Name3" })
person4: person_upsert(data: { id: "ef8de2a4a6de400e94d555f148b643c0", name: "Name4" })
person5: person_upsert(data: { id: "8584fd7ca2b6453da18d21d4341f1804", name: "Name5" })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@file:OptIn(ExperimentalFirebaseDataConnect::class)

package com.google.firebase.dataconnect

import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase
import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema
import io.kotest.assertions.withClue
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.next
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import org.junit.Test

class UnknownKeysIntegrationTest : DataConnectIntegrationTestBase() {

private val personSchema by lazy { PersonSchema(dataConnectFactory) }
private val allTypesSchema by lazy { AllTypesSchema(dataConnectFactory) }

@Test
fun unknownKeysInQueryResponseDataShouldBeIgnored() = runTest {
val id = Arb.dataConnect.uuid().next()
allTypesSchema
.createPrimitive(
AllTypesSchema.PrimitiveData(
id = id,
idFieldNullable = "0d2fcdf1c4a84c64a87f3c7932b31749",
intField = 42,
intFieldNullable = 43,
floatField = 123.45,
floatFieldNullable = 678.91,
booleanField = true,
booleanFieldNullable = false,
stringField = "TestString",
stringFieldNullable = "TestNullableString"
)
)
.execute()

@Serializable
data class PrimitiveQueryDataValues(
val intFieldNullable: Int?,
val booleanField: Boolean,
val booleanFieldNullable: Boolean?,
val stringField: String,
val stringFieldNullable: String?,
)

/** An adaptation of [AllTypesSchema.GetPrimitiveQuery.Data] with some keys missing. */
@Serializable
data class PrimitiveQueryDataMissingSomeKeys(val primitive: PrimitiveQueryDataValues)

val result =
allTypesSchema
.getPrimitive(id = id)
.withDataDeserializer(serializer<PrimitiveQueryDataMissingSomeKeys>())
.execute()

result.data.primitive shouldBe
PrimitiveQueryDataValues(
intFieldNullable = 43,
booleanField = true,
booleanFieldNullable = false,
stringField = "TestString",
stringFieldNullable = "TestNullableString"
)
}

@Test
fun unknownKeysInMutationResponseDataShouldBeIgnored() = runTest {
@Serializable data class PersonKeyWithId(val id: String)
val person1 = PersonKeyWithId(id = "8a218894521f457a9be45a0986494058")
val person4 = PersonKeyWithId(id = "ef8de2a4a6de400e94d555f148b643c0")
val mutationRef =
personSchema.dataConnect.mutation("create5People", Unit, serializer<Unit>(), serializer())

// Precondition check: Verify that the response contains person1..person5 and the expected IDs.
withClue("precondition check") {
@Serializable
data class Create5PeopleData(
val person1: PersonKeyWithId,
val person3: PersonKeyWithId,
val person2: PersonKeyWithId,
val person4: PersonKeyWithId,
val person5: PersonKeyWithId,
)

val mutationResult =
mutationRef.withDataDeserializer(serializer<Create5PeopleData>()).execute()
mutationResult.data shouldBe
Create5PeopleData(
person1 = person1,
person2 = PersonKeyWithId(id = "464443371f284194be4b2e78c3ef000c"),
person3 = PersonKeyWithId(id = "903d83db81754bd29860458f127ef124"),
person4 = person4,
person5 = PersonKeyWithId(id = "8584fd7ca2b6453da18d21d4341f1804"),
)
}

withClue("actual test") {
// Create5PeopleDataWithMissingKeys is missing "person2.id", "person3", and "person5" which
// will be present in the response.
@Serializable data class PersonKeyWithoutId(val foo: Nothing? = null)
@Serializable
data class Create5PeopleDataWithMissingKeys(
val person1: PersonKeyWithId,
val person2: PersonKeyWithoutId,
val person4: PersonKeyWithId,
)

val mutationResult =
mutationRef.withDataDeserializer(serializer<Create5PeopleDataWithMissingKeys>()).execute()
mutationResult.data shouldBe
Create5PeopleDataWithMissingKeys(
person1 = person1,
person2 = PersonKeyWithoutId(),
person4 = PersonKeyWithId(id = "ef8de2a4a6de400e94d555f148b643c0"),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,12 @@ private class ProtoStructValueDecoder(
addAll(struct.fieldsMap.keys)
addAll(descriptor.elementNames)
}
elementIndexes = names.map(descriptor::getElementIndex).sorted().iterator()
elementIndexes =
names
.map(descriptor::getElementIndex)
.filter { it != CompositeDecoder.UNKNOWN_NAME } // ignore unknown keys
.sorted()
.iterator()
}

return elementIndexes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package com.google.firebase.dataconnect

import com.google.firebase.dataconnect.SerializationTestData.serializationTestDataAllTypes
import com.google.firebase.dataconnect.SerializationTestData.withEmptyListOfUnitRecursive
import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
import com.google.firebase.dataconnect.testutil.property.arbitrary.twoValues
import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase
import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto
import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromStruct
Expand Down Expand Up @@ -477,6 +479,21 @@ class ProtoStructDecoderUnitTest {
}
}

@Test
fun `decodeFromStruct() ignores unknown struct keys`() = runTest {
@Serializable data class TestData1(val value1: String, val value2: String)
@Serializable data class TestData2(val value1: String)

val testData1Arb: Arb<TestData1> =
Arb.twoValues(Arb.dataConnect.string()).map { (value1, value2) -> TestData1(value1, value2) }

checkAll(propTestConfig, testData1Arb) { testData1 ->
val struct = encodeToStruct(testData1)
val decodedTestData = decodeFromStruct<TestData2>(struct)
decodedTestData shouldBe TestData2(testData1.value1)
}
}

@Test
fun `decodeFromStruct() should throw SerializationException if attempting to decode an Int`() {
assertDecodeFromStructThrowsIncorrectKindCase<Int>(
Expand Down
Loading