diff --git a/firebase-dataconnect/connectors/connectors.gradle.kts b/firebase-dataconnect/connectors/connectors.gradle.kts index 80e8aeca1bb..82b46f040f1 100644 --- a/firebase-dataconnect/connectors/connectors.gradle.kts +++ b/firebase-dataconnect/connectors/connectors.gradle.kts @@ -73,6 +73,7 @@ dependencies { testImplementation(libs.kotest.assertions) testImplementation(libs.kotest.property) testImplementation(libs.kotlin.coroutines.test) + testImplementation(libs.kotlinx.serialization.json) testImplementation(libs.mockk) testImplementation(libs.robolectric) diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumIntegrationTest.kt new file mode 100644 index 00000000000..2bfa71fa77d --- /dev/null +++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumIntegrationTest.kt @@ -0,0 +1,546 @@ +/* + * 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.connectors.demo + +import com.google.firebase.dataconnect.ExperimentalFirebaseDataConnect +import com.google.firebase.dataconnect.connectors.demo.EnumValue.Known +import com.google.firebase.dataconnect.connectors.demo.EnumValue.Unknown +import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.property.arbitrary.threeValues +import com.google.firebase.dataconnect.testutil.property.arbitrary.twoValues +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.bind +import io.kotest.property.arbitrary.constant +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.orNull +import io.kotest.property.checkAll +import java.util.UUID +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Test + +class EnumIntegrationTest : DemoConnectorIntegrationTestBase() { + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNonNullableNonNullKnownEnumValue() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val key = connector.enumNonNullableInsert.execute(enumValue).data.key + val queryResult = connector.enumNonNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe Known(enumValue) } + } + } + + @Test + fun updateNonNullableEnumValue() = runTest { + checkAll(NUM_ITERATIONS, Arb.twoValues(Arb.enum())) { values -> + val (value1, value2) = values + val key = connector.enumNonNullableInsert.execute(value1).data.key + val updateResult = connector.enumNonNullableUpdateByKey.execute(key) { value = value2 } + withClue(updateResult) { updateResult.data.key shouldBe key } + val queryResult = connector.enumNonNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe Known(value2) } + } + } + + @Test + fun queryNonNullableNonNullUnknownEnumValue() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val key = connector.enumNonNullableInsert.execute(enumValue).data.key + val queryRef = + connector.enumNonNullableGetByKey + .ref(key) + .withDataDeserializer(EnumSubsetGetByKeyQuery.dataDeserializer) + val queryResult = queryRef.execute().data + val expectedEnumValue: EnumValue = enumValue.toN5ekmae3jnSubsetEnumValue() + withClue(queryResult) { queryResult.item?.value shouldBe expectedEnumValue } + } + } + + @Test + fun queryNonNullableByNonNullEnumValue() = runTest { + checkAll(NUM_ITERATIONS, Arb.insert3TestData(Arb.enum())) { testData -> + val (tag, insertValue1, insertValue2, insertValue3, queryValue) = testData + val insertResult = + connector.enumNonNullableInsert3.execute(tag, insertValue1, insertValue2, insertValue3).data + val queryResult = connector.enumNonNullableGetAllByTagAndValue.execute(tag, queryValue).data + val matchingIds = insertResult.idsForMatchingValues(testData) + withClue(queryResult) { + queryResult.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + } + + @Test + fun queryNonNullableByUndefinedEnumValue() = runTest { + checkAll(NUM_ITERATIONS, Arb.insert3TestData(Arb.enum())) { testData -> + val (tag, insertValue1, insertValue2, insertValue3) = testData + val insertResult = + connector.enumNonNullableInsert3.execute(tag, insertValue1, insertValue2, insertValue3).data + val queryResult = connector.enumNonNullableGetAllByTagAndMaybeValue.execute(tag).data + withClue(queryResult) { + queryResult.items.map { it.id } shouldContainExactlyInAnyOrder insertResult.ids + } + } + } + + @Test + fun queryNonNullableByNullEnumValue() = runTest { + checkAll(NUM_ITERATIONS, Arb.insert3TestData(Arb.enum())) { testData -> + val (tag, insertValue1, insertValue2, insertValue3) = testData + connector.enumNonNullableInsert3.execute(tag, insertValue1, insertValue2, insertValue3) + val queryResult = + connector.enumNonNullableGetAllByTagAndMaybeValue.execute(tag) { value = null }.data + withClue(queryResult) { queryResult.items.shouldBeEmpty() } + } + } + + @Test + fun queryNonNullableByDefaultEnumValue() = runTest { + val enumArb = Arb.enum() + val queryValueArb = Arb.constant(N5ekmae3jn.XGWGVMYTHJ) + checkAll(NUM_ITERATIONS, Arb.insert3TestData(enumArb, queryValue = queryValueArb)) { testData -> + val (tag, insertValue1, insertValue2, insertValue3) = testData + val insertResult = + connector.enumNonNullableInsert3.execute(tag, insertValue1, insertValue2, insertValue3).data + val queryResult = connector.enumNonNullableGetAllByTagAndDefaultValue.execute(tag).data + val matchingIds = insertResult.idsForMatchingValues(testData) + withClue(queryResult) { + queryResult.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNullableNonNullEnumValue() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val key = connector.enumNullableInsert.execute { value = enumValue }.data.key + val queryResult = connector.enumNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe Known(enumValue) } + } + } + + @Test + fun insertNullableNullEnumValue() = runTest { + val key = connector.enumNullableInsert.execute { value = null }.data.key + val queryResult = connector.enumNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value.shouldBeNull() } + } + + @Test + fun updateNullableEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.twoValues(enumArb)) { values -> + val (value1, value2) = values + val key = connector.enumNullableInsert.execute { value = value1 }.data.key + connector.enumNullableUpdateByKey.execute(key) { value = value2 } + val queryResult = connector.enumNullableGetByKey.execute(key).data + withClue(queryResult) { + if (value2 === null) { + queryResult.item?.value.shouldBeNull() + } else { + queryResult.item?.value shouldBe Known(value2) + } + } + } + } + + @Test + fun queryNullableNonNullUnknownEnumValue() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val key = connector.enumNullableInsert.execute { value = enumValue }.data.key + val queryRef = + connector.enumNullableGetByKey + .ref(key) + .withDataDeserializer(EnumSubsetGetByKeyQuery.dataDeserializer) + val queryResult = queryRef.execute().data + val expectedEnumValue: EnumValue = enumValue.toN5ekmae3jnSubsetEnumValue() + withClue(queryResult) { queryResult.item?.value shouldBe expectedEnumValue } + } + } + + @Test + fun queryNullableByEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.33) + checkAll(NUM_ITERATIONS, Arb.insert3TestData(enumArb)) { testData -> + val (tag, insertValue1, insertValue2, insertValue3, queryValue) = testData + val insertResult = + connector.enumNullableInsert3 + .execute(tag) { + value1 = insertValue1 + value2 = insertValue2 + value3 = insertValue3 + } + .data + val queryResult = + connector.enumNullableGetAllByTagAndValue.execute(tag) { value = queryValue }.data + val matchingIds = insertResult.idsForMatchingValues(testData) + withClue(queryResult) { + queryResult.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + } + + @Test + fun queryNullableByUndefinedEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.insert3TestData(enumArb)) { testData -> + val (tag, insertValue1, insertValue2, insertValue3) = testData + val insertResult = + connector.enumNullableInsert3 + .execute(tag) { + value1 = insertValue1 + value2 = insertValue2 + value3 = insertValue3 + } + .data + val queryResult = connector.enumNullableGetAllByTagAndValue.execute(tag).data + withClue(queryResult) { + queryResult.items.map { it.id } shouldContainExactlyInAnyOrder insertResult.ids + } + } + } + + @Test + fun queryNullableByDefaultEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.33) + val queryValueArb = Arb.constant(N5ekmae3jn.QJX7C7RD5T) + checkAll(NUM_ITERATIONS, Arb.insert3TestData(enumArb, queryValue = queryValueArb)) { testData -> + val (tag, insertValue1, insertValue2, insertValue3) = testData + val insertResult = + connector.enumNullableInsert3 + .execute(tag) { + value1 = insertValue1 + value2 = insertValue2 + value3 = insertValue3 + } + .data + val queryResult = connector.enumNullableGetAllByTagAndDefaultValue.execute(tag).data + val matchingIds = insertResult.idsForMatchingValues(testData) + withClue(queryResult) { + queryResult.items.map { it.id } shouldContainExactlyInAnyOrder matchingIds + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullableTableDefault table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertEnumNonNullableTableDefault() = runTest { + val key = connector.enumNonNullableTableDefaultInsert.execute().data.key + val queryResult = connector.enumNonNullableTableDefaultGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe Known(N5ekmae3jn.RGTB44C2M8) } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullableTableDefault table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertEnumNullableTableDefault() = runTest { + val key = connector.enumNullableTableDefaultInsert.execute().data.key + val queryResult = connector.enumNullableTableDefaultGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe Known(N5ekmae3jn.ZE6Z5778RV) } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullableListOfNonNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNonNullableListOfNonNullable() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { values -> + val key = connector.enumNonNullableListOfNonNullableInsert.execute(values).data.key + val queryResult = connector.enumNonNullableListOfNonNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe values.map(::Known) } + } + } + + @Test + fun queryNonNullableListOfNonNullableUnknownEnumValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val key = connector.enumNonNullableListOfNonNullableInsert.execute(values).data.key + val queryRef = + connector.enumNonNullableListOfNonNullableGetByKey + .ref(key) + .withDataDeserializer(EnumSubsetListGetByKeyQuery.dataDeserializer) + val expectedEnumValues: List> = + values.map { it.toN5ekmae3jnSubsetEnumValue() } + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult.item?.value shouldBe expectedEnumValues } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullableListOfNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNonNullableListOfNullable() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { values -> + val key = connector.enumNonNullableListOfNullableInsert.execute(values).data.key + val queryResult = connector.enumNonNullableListOfNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe values.map(::Known) } + } + } + + @Test + fun queryNonNullableListOfNullableUnknownEnumValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val key = connector.enumNonNullableListOfNullableInsert.execute(values).data.key + val queryRef = + connector.enumNonNullableListOfNullableGetByKey + .ref(key) + .withDataDeserializer(EnumSubsetListGetByKeyQuery.dataDeserializer) + val expectedEnumValues: List> = + values.map { it.toN5ekmae3jnSubsetEnumValue() } + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult.item?.value shouldBe expectedEnumValues } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullableListOfNonNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNullableListOfNonNullable_NonNullList() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { values -> + val key = connector.enumNullableListOfNonNullableInsert.execute { value = values }.data.key + val queryResult = connector.enumNullableListOfNonNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe values.map(::Known) } + } + } + + @Test + fun insertNullableListOfNonNullable_NullList() = runTest { + val key = connector.enumNullableListOfNonNullableInsert.execute { value = null }.data.key + val queryResult = connector.enumNullableListOfNonNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value.shouldBeNull() } + } + + @Test + fun queryNullableListOfNonNullableUnknownEnumValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val key = connector.enumNullableListOfNonNullableInsert.execute { value = values }.data.key + val queryRef = + connector.enumNullableListOfNonNullableGetByKey + .ref(key) + .withDataDeserializer(EnumSubsetListGetByKeyQuery.dataDeserializer) + val expectedEnumValues: List> = + values.map { it.toN5ekmae3jnSubsetEnumValue() } + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult.item?.value shouldBe expectedEnumValues } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullableListOfNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNullableListOfNullable_NonNullList() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { values -> + val key = connector.enumNullableListOfNullableInsert.execute { value = values }.data.key + val queryResult = connector.enumNullableListOfNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe values.map(::Known) } + } + } + + @Test + fun insertNullableListOfNullable_NullList() = runTest { + val key = connector.enumNullableListOfNullableInsert.execute { value = null }.data.key + val queryResult = connector.enumNullableListOfNullableGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value.shouldBeNull() } + } + + @Test + fun queryNullableListOfNullableUnknownEnumValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val key = connector.enumNullableListOfNullableInsert.execute { value = values }.data.key + val queryRef = + connector.enumNullableListOfNullableGetByKey + .ref(key) + .withDataDeserializer(EnumSubsetListGetByKeyQuery.dataDeserializer) + val expectedEnumValues: List> = + values.map { it.toN5ekmae3jnSubsetEnumValue() } + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult.item?.value shouldBe expectedEnumValues } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumKotlinKeywords table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun enumKotlinKeywords() = runTest { + Break.entries.forEach { enumValue -> + val key = connector.enumKotlinKeywordsInsert.execute(enumValue).data.key + val queryResult = connector.enumKotlinKeywordsGetByKey.execute(key).data + withClue(queryResult) { queryResult.item?.value shouldBe Known(enumValue) } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumKey table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + @Ignore( + "TODO(b/432793533) Re-enable this test once the emulator crash " + + "caused by the \"EnumKey_GetByKey\" query is fixed." + ) + fun enumAsPrimaryKey() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val tagValue = Arb.dataConnect.tag().next(rs) + val key = connector.enumKeyInsert.execute(enumValue) { tag = tagValue }.data.key + withClue(key) { key.enumValue shouldBe Known(enumValue) } + // TODO(b/432793533): Uncomment once the "EnumKey_GetByKey" query is uncommented. + // val queryResult = connector.enumKeyGetByKey.execute(key).data + // withClue(queryResult) { queryResult.item?.tag shouldBe tagValue } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for MultipleEnumColumns table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + @Ignore( + "TODO(cl/785956954) Re-enable this test once testing against a data connect " + + "emulator that fixes serialization when more than one user-defined enum type " + + "in the data" + ) + fun multipleEnumColumns() = runTest { + checkAll(NUM_ITERATIONS, Arb.enum(), Arb.enum()) { enum1, enum2 -> + val key = connector.multipleEnumColumnsInsert.execute(enum1, enum2).data.key + val queryResult = connector.multipleEnumColumnsGetByKey.execute(key).data + withClue(queryResult) { + assertSoftly { + queryResult.item?.enum1 shouldBe Known(enum1) + queryResult.item?.enum2 shouldBe Known(enum2) + } + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Helper classes and functions. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + private data class Insert3TestData( + val tag: String, + val value1: T, + val value2: T, + val value3: T, + val queryValue: T + ) + + private companion object { + + /** The default number of iterations to use in property-based tests. */ + const val NUM_ITERATIONS = 10 + + fun N5ekmae3jn.toN5ekmae3jnSubsetOrNull(): N5ekmae3jnSubset? = + when (this) { + N5ekmae3jn.DPSKD6HR3A -> N5ekmae3jnSubset.DPSKD6HR3A + N5ekmae3jn.XGWGVMYTHJ -> N5ekmae3jnSubset.XGWGVMYTHJ + N5ekmae3jn.QJX7C7RD5T -> N5ekmae3jnSubset.QJX7C7RD5T + else -> null + } + + fun N5ekmae3jn.toN5ekmae3jnSubsetEnumValue(): EnumValue { + return Known(toN5ekmae3jnSubsetOrNull() ?: return Unknown(name)) + } + + @Suppress("NAME_SHADOWING") + fun Arb.Companion.insert3TestData( + insertValues: Arb, + queryValue: Arb = insertValues, + tag: Arb = Arb.dataConnect.tag(), + ): Arb> = + bind(tag, Arb.threeValues(insertValues), queryValue) { tag, insertValues, queryValue -> + Insert3TestData( + tag, + insertValues.value1, + insertValues.value2, + insertValues.value3, + queryValue + ) + } + + val EnumNonNullableInsert3Mutation.Data.ids: List + get() = listOf(key1, key2, key3).map { it.id } + + val EnumNullableInsert3Mutation.Data.ids: List + get() = listOf(key1, key2, key3).map { it.id } + + fun EnumNonNullableInsert3Mutation.Data.idsForMatchingValues( + testData: Insert3TestData + ): List = keysForMatchingValues(testData, key1, key2, key3).map { it.id } + + fun EnumNullableInsert3Mutation.Data.idsForMatchingValues( + testData: Insert3TestData + ): List = keysForMatchingValues(testData, key1, key2, key3).map { it.id } + + private fun keysForMatchingValues( + testData: Insert3TestData, + key1: K, + key2: K, + key3: K, + ): List = buildList { + if (testData.value1 == testData.queryValue) { + add(key1) + } + if (testData.value2 == testData.queryValue) { + add(key2) + } + if (testData.value3 == testData.queryValue) { + add(key3) + } + } + } +} diff --git a/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueKnownUnitTest.kt b/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueKnownUnitTest.kt new file mode 100644 index 00000000000..e70c2436006 --- /dev/null +++ b/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueKnownUnitTest.kt @@ -0,0 +1,168 @@ +/* + * 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(ExperimentalKotest::class) + +package com.google.firebase.dataconnect.connectors.demo + +import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.of +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +class EnumValueKnownUnitTest { + + @Test + fun `constructor() should set properties to corresponding arguments`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.value shouldBeSameInstanceAs enum + } + } + + @Test + fun `stringValue property should be the name of the enum`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.stringValue shouldBe enum.name + } + } + + @Test + fun `equals() should return true when invoked with itself`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.equals(enumValue) shouldBe true + } + } + + @Test + fun `equals() should return true when invoked with a distinct, but equal, instance`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue1 = EnumValue.Known(enum) + val enumValue2 = EnumValue.Known(enum) + enumValue1.equals(enumValue2) shouldBe true + } + } + + @Test + fun `equals() should return false when invoked with null`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false when invoked with a different type`() = runTest { + val others = Arb.of("foo", 42, java.time.LocalDate.now()) + checkAll(propTestConfig, Arb.enum(), others) { enum, other -> + val enumValue = EnumValue.Known(enum) + enumValue.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when the enum differs`() = runTest { + checkAll(propTestConfig, Arb.enum().distinctPair()) { (enum1, enum2) -> + val enumValue1 = EnumValue.Known(enum1) + val enumValue2 = EnumValue.Known(enum2) + enumValue1.equals(enumValue2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + val hashCode = enumValue.hashCode() + repeat(5) { withClue("iteration=$it") { enumValue.hashCode() shouldBe hashCode } } + } + } + + @Test + fun `hashCode() should return the same value when invoked on equal, but distinct, objects`() = + runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue1 = EnumValue.Known(enum) + val enumValue2 = EnumValue.Known(enum) + enumValue1.hashCode() shouldBe enumValue2.hashCode() + } + } + + @Test + fun `hashCode() should return different values for different enum values`() = runTest { + checkAll(propTestConfig, Arb.enum().distinctPair()) { (enum1, enum2) -> + assume(enum1.hashCode() != enum2.hashCode()) + val enumValue1 = EnumValue.Known(enum1) + val enumValue2 = EnumValue.Known(enum2) + enumValue1.hashCode() shouldNotBe enumValue2.hashCode() + } + } + + @Test + fun `toString() should return a string conforming to what is expected`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.toString() shouldBe "Known(${enum.name})" + } + } + + @Test + fun `copy() with no arguments should return an equal, but distinct, instance`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + val enumValueCopy = enumValue.copy() + enumValue shouldBe enumValueCopy + enumValue shouldNotBeSameInstanceAs enumValueCopy + } + } + + @Test + fun `copy() with all arguments should return a new instance with the given arguments`() = + runTest { + checkAll(propTestConfig, Arb.enum().distinctPair()) { (enum1, enum2) -> + val enumValue1 = EnumValue.Known(enum1) + val enumValue2 = enumValue1.copy(enum2) + enumValue2 shouldBe EnumValue.Known(enum2) + } + } + + @Suppress("unused") + private enum class Food { + Burrito, + Cake, + Pizza, + Shawarma, + Sushi, + } + + private companion object { + val propTestConfig = PropTestConfig(iterations = 50) + } +} diff --git a/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueSerializerUnitTest.kt b/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueSerializerUnitTest.kt new file mode 100644 index 00000000000..511bd592ba7 --- /dev/null +++ b/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueSerializerUnitTest.kt @@ -0,0 +1,102 @@ +/* + * 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(ExperimentalKotest::class, ExperimentalSerializationApi::class) + +package com.google.firebase.dataconnect.connectors.demo + +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.map +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import org.junit.Test + +class EnumValueSerializerUnitTest { + + @Test + fun `serialize() should produce the expected serialized string for _known_ values`() = runTest { + checkAll(propTestConfig, Arb.knownEnumValue()) { knownEnumValue: EnumValue.Known -> + val encodedValue = Json.encodeToJsonElement(Dog.serializer, knownEnumValue) + encodedValue.jsonPrimitive.content shouldBe knownEnumValue.value.name + } + } + + @Test + fun `serialize() should produce the expected serialized string for _unknown_ values`() = runTest { + checkAll(propTestConfig, Arb.unknownEnumValue()) { unknownEnumValue: EnumValue.Unknown -> + val encodedValue = Json.encodeToJsonElement(Dog.serializer, unknownEnumValue) + encodedValue.jsonPrimitive.content shouldBe unknownEnumValue.stringValue + } + } + + @Test + fun `deserialize() should produce the expected EnumValue object for _known_ values`() = runTest { + checkAll(propTestConfig, Arb.knownEnumValue()) { knownEnumValue: EnumValue.Known -> + val encodedValue = JsonPrimitive(knownEnumValue.value.name) + val decodedEnumValue = Json.decodeFromJsonElement(Dog.serializer, encodedValue) + decodedEnumValue shouldBe knownEnumValue + } + } + + @Test + fun `deserialize() should produce the expected EnumValue object for _unknown_ values`() = + runTest { + checkAll(propTestConfig, Arb.unknownEnumValue()) { unknownEnumValue: EnumValue.Unknown -> + val encodedValue = JsonPrimitive(unknownEnumValue.stringValue) + val decodedEnumValue = Json.decodeFromJsonElement(Dog.serializer, encodedValue) + decodedEnumValue shouldBe unknownEnumValue + } + } + + @Suppress("unused") + enum class Dog { + Boxer, + Bulldog, + Dachshund, + Labrador, + Poodle; + + companion object { + val serializer: KSerializer> = EnumValueSerializer(Dog.entries) + } + } + + private companion object { + val propTestConfig = + PropTestConfig( + iterations = 500, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2) + ) + + fun Arb.Companion.unknownEnumValue( + stringValue: Arb = Arb.dataConnect.string() + ): Arb = stringValue.map { EnumValue.Unknown(it) } + + fun Arb.Companion.knownEnumValue( + enumValue: Arb = Arb.enum() + ): Arb> = enumValue.map { EnumValue.Known(it) } + } +} diff --git a/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueUnknownUnitTest.kt b/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueUnknownUnitTest.kt new file mode 100644 index 00000000000..0bdc697c141 --- /dev/null +++ b/firebase-dataconnect/connectors/src/test/kotlin/com/google/firebase/dataconnect/connectors/demo/EnumValueUnknownUnitTest.kt @@ -0,0 +1,169 @@ +/* + * 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(ExperimentalKotest::class) + +package com.google.firebase.dataconnect.connectors.demo + +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.of +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +class EnumValueUnknownUnitTest { + + @Test + fun `constructor() should set properties to corresponding arguments`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.stringValue shouldBeSameInstanceAs stringValue + } + } + + @Test + @Ignore( + "TODO(cl/785477120) Enable this test once a data connect emulator build that " + + "includes cl/785477120 is released, which will have a version >2.10.0 and " + + "a firebase-tools version >14.11.0" + ) + fun `value property should unconditionally be null`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + // TODO(cl/785477120) Uncomment the line below when the test is re-enabled. + // enumValue.value.shouldBeNull() + } + } + + @Test + fun `equals() should return true when invoked with itself`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.equals(enumValue) shouldBe true + } + } + + @Test + fun `equals() should return true when invoked with a distinct, but equal, instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue1 = EnumValue.Unknown(stringValue) + val enumValue2 = EnumValue.Unknown(stringValue) + enumValue1.equals(enumValue2) shouldBe true + } + } + + @Test + fun `equals() should return false when invoked with null`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false when invoked with a different type`() = runTest { + val others = Arb.of("foo", 42, java.time.LocalDate.now()) + checkAll(propTestConfig, Arb.dataConnect.string(), others) { stringValue, other -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when the stringValue differs`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string().distinctPair()) { (stringValue1, stringValue2) + -> + val enumValue1 = EnumValue.Unknown(stringValue1) + val enumValue2 = EnumValue.Unknown(stringValue2) + enumValue1.equals(enumValue2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + val hashCode = enumValue.hashCode() + repeat(5) { withClue("iteration=$it") { enumValue.hashCode() shouldBe hashCode } } + } + } + + @Test + fun `hashCode() should return the same value when invoked on equal, but distinct, objects`() = + runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue1 = EnumValue.Unknown(stringValue) + val enumValue2 = EnumValue.Unknown(stringValue) + enumValue1.hashCode() shouldBe enumValue2.hashCode() + } + } + + @Test + fun `hashCode() should return different values for different stringValue values`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string().distinctPair()) { (stringValue1, stringValue2) + -> + assume(stringValue1.hashCode() != stringValue2.hashCode()) + val enumValue1 = EnumValue.Unknown(stringValue1) + val enumValue2 = EnumValue.Unknown(stringValue2) + enumValue1.hashCode() shouldNotBe enumValue2.hashCode() + } + } + + @Test + fun `toString() should return a string conforming to what is expected`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.toString() shouldBe "Unknown($stringValue)" + } + } + + @Test + fun `copy() with no arguments should return an equal, but distinct, instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + val enumValueCopy = enumValue.copy() + enumValue shouldBe enumValueCopy + enumValue shouldNotBeSameInstanceAs enumValueCopy + } + } + + @Test + fun `copy() with all arguments should return a new instance with the given arguments`() = + runTest { + checkAll(propTestConfig, Arb.dataConnect.string().distinctPair()) { + (stringValue1, stringValue2) -> + val enumValue1 = EnumValue.Unknown(stringValue1) + val enumValue2 = enumValue1.copy(stringValue2) + enumValue2 shouldBe EnumValue.Unknown(stringValue2) + } + } + + private companion object { + val propTestConfig = PropTestConfig(iterations = 50) + } +} diff --git a/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql b/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql index 907f95778e4..231a0991214 100644 --- a/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql +++ b/firebase-dataconnect/emulator/dataconnect/connector/demo/demo_ops.gql @@ -1581,3 +1581,216 @@ query ExprValues_GetByKey($key: ExprValues_Key!) @auth(level: PUBLIC) { requestTimeAsDate } } + +############################################################################### +# Operations for table: type EnumNonNullable +############################################################################### + +mutation EnumNonNullable_Insert($value: N5ekmae3jn!) @auth(level: PUBLIC) { + key: enumNonNullable_insert(data: { value: $value }) +} + +mutation EnumNonNullable_Insert3($tag: String!, $value1: N5ekmae3jn!, $value2: N5ekmae3jn!, $value3: N5ekmae3jn!) @auth(level: PUBLIC) { + key1: enumNonNullable_insert(data: { value: $value1, tag: $tag }) + key2: enumNonNullable_insert(data: { value: $value2, tag: $tag }) + key3: enumNonNullable_insert(data: { value: $value3, tag: $tag }) +} + +query EnumNonNullable_GetByKey($key: EnumNonNullable_Key!) @auth(level: PUBLIC) { + item: enumNonNullable(key: $key) { value } +} + +query EnumNonNullable_GetAllByTagAndValue($tag: String!, $value: N5ekmae3jn!) @auth(level: PUBLIC) { + items: enumNonNullables( + limit: 5, + where: { value: { eq: $value }, tag: { eq: $tag } }, + ) { id } +} + +query EnumNonNullable_GetAllByTagAndMaybeValue($tag: String!, $value: N5ekmae3jn) @auth(level: PUBLIC) { + items: enumNonNullables( + limit: 5, + where: { value: { eq: $value }, tag: { eq: $tag } }, + ) { id } +} + +query EnumNonNullable_GetAllByTagAndDefaultValue($tag: String!, $value: N5ekmae3jn! = XGWGVMYTHJ) @auth(level: PUBLIC) { + items: enumNonNullables( + limit: 5, + where: { value: { eq: $value }, tag: { eq: $tag } }, + ) { id } +} + +mutation EnumNonNullable_UpdateByKey($key: EnumNonNullable_Key!, $value: N5ekmae3jn) @auth(level: PUBLIC) { + enumNonNullable_update(key: $key, data: { value: $value }) +} + +############################################################################### +# Operations for table: type EnumNullable +############################################################################### + +mutation EnumNullable_Insert($value: N5ekmae3jn) @auth(level: PUBLIC) { + key: enumNullable_insert(data: { value: $value }) +} + +mutation EnumNullable_Insert3($tag: String!, $value1: N5ekmae3jn, $value2: N5ekmae3jn, $value3: N5ekmae3jn) @auth(level: PUBLIC) { + key1: enumNullable_insert(data: { value: $value1, tag: $tag }) + key2: enumNullable_insert(data: { value: $value2, tag: $tag }) + key3: enumNullable_insert(data: { value: $value3, tag: $tag }) +} + +query EnumNullable_GetByKey($key: EnumNullable_Key!) @auth(level: PUBLIC) { + item: enumNullable(key: $key) { value } +} + +query EnumNullable_GetAllByTagAndValue($tag: String!, $value: N5ekmae3jn) @auth(level: PUBLIC) { + items: enumNullables( + limit: 5, + where: { value: { eq: $value }, tag: { eq: $tag } }, + ) { id } +} + +query EnumNullable_GetAllByTagAndMaybeValue($tag: String!, $value: N5ekmae3jn) @auth(level: PUBLIC) { + items: enumNullables( + limit: 5, + where: { value: { eq: $value }, tag: { eq: $tag } }, + ) { id } +} + +query EnumNullable_GetAllByTagAndDefaultValue($tag: String!, $value: N5ekmae3jn = QJX7C7RD5T) @auth(level: PUBLIC) { + items: enumNullables( + limit: 5, + where: { value: { eq: $value }, tag: { eq: $tag } }, + ) { id } +} + +mutation EnumNullable_UpdateByKey($key: EnumNullable_Key!, $value: N5ekmae3jn) @auth(level: PUBLIC) { + enumNullable_update(key: $key, data: { value: $value }) +} + +############################################################################### +# Operations for table: type EnumNonNullableTableDefault +############################################################################### + +mutation EnumNonNullableTableDefault_Insert @auth(level: PUBLIC) { + key: enumNonNullableTableDefault_insert(data: { }) +} + +query EnumNonNullableTableDefault_GetByKey($key: EnumNonNullableTableDefault_Key!) @auth(level: PUBLIC) { + item: enumNonNullableTableDefault(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumNullableTableDefault +############################################################################### + +mutation EnumNullableTableDefault_Insert @auth(level: PUBLIC) { + key: enumNullableTableDefault_insert(data: { }) +} + +query EnumNullableTableDefault_GetByKey($key: EnumNullableTableDefault_Key!) @auth(level: PUBLIC) { + item: enumNullableTableDefault(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumNonNullableListOfNonNullable +############################################################################### + +mutation EnumNonNullableListOfNonNullable_Insert($value: [N5ekmae3jn!]!) @auth(level: PUBLIC) { + key: enumNonNullableListOfNonNullable_insert(data: { value: $value }) +} + +query EnumNonNullableListOfNonNullable_GetByKey($key: EnumNonNullableListOfNonNullable_Key!) @auth(level: PUBLIC) { + item: enumNonNullableListOfNonNullable(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumNonNullableListOfNullable +############################################################################### + +mutation EnumNonNullableListOfNullable_Insert($value: [N5ekmae3jn!]!) @auth(level: PUBLIC) { + key: enumNonNullableListOfNullable_insert(data: { value: $value }) +} + +query EnumNonNullableListOfNullable_GetByKey($key: EnumNonNullableListOfNullable_Key!) @auth(level: PUBLIC) { + item: enumNonNullableListOfNullable(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumNullableListOfNonNullable +############################################################################### + +mutation EnumNullableListOfNonNullable_Insert($value: [N5ekmae3jn!]) @auth(level: PUBLIC) { + key: enumNullableListOfNonNullable_insert(data: { value: $value }) +} + +query EnumNullableListOfNonNullable_GetByKey($key: EnumNullableListOfNonNullable_Key!) @auth(level: PUBLIC) { + item: enumNullableListOfNonNullable(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumNullableListOfNullable +############################################################################### + +mutation EnumNullableListOfNullable_Insert($value: [N5ekmae3jn!]) @auth(level: PUBLIC) { + key: enumNullableListOfNullable_insert(data: { value: $value }) +} + +query EnumNullableListOfNullable_GetByKey($key: EnumNullableListOfNullable_Key!) @auth(level: PUBLIC) { + item: enumNullableListOfNullable(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumKotlinKeywords +############################################################################### + +mutation EnumKotlinKeywords_Insert($value: break!) @auth(level: PUBLIC) { + key: enumKotlinKeywords_insert(data: { value: $value }) +} + +query EnumKotlinKeywords_GetByKey($key: EnumKotlinKeywords_Key!) @auth(level: PUBLIC) { + item: enumKotlinKeywords(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumSubset +############################################################################### + +# Note: This query is never actually executed in the tests; rather, its generated code is used. +query EnumSubset_GetByKey($key: EnumSubset_Key!) @auth(level: PUBLIC) { + item: enumSubset(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumSubsetList +############################################################################### + +# Note: This query is never actually executed in the tests; rather, its generated code is used. +query EnumSubsetList_GetByKey($key: EnumSubsetList_Key!) @auth(level: PUBLIC) { + item: enumSubsetList(key: $key) { value } +} + +############################################################################### +# Operations for table: type EnumKey +############################################################################### + +mutation EnumKey_Insert($enumValue: N5ekmae3jn!, $tag: String) @auth(level: PUBLIC) { + key: enumKey_insert(data: { enumValue: $enumValue, tag: $tag }) +} + +# TODO(b/432793533) Uncomment once the emulator crash caused by this query is fixed. +#query EnumKey_GetByKey($key: EnumKey_Key!) @auth(level: PUBLIC) { +# item: enumKey(key: $key) { tag } +#} + +############################################################################### +# Operations for table: type MultipleEnumColumns +############################################################################### + +mutation MultipleEnumColumns_Insert($enum1: N5ekmae3jn!, $enum2: S7yayynb25!) @auth(level: PUBLIC) { + key: multipleEnumColumns_insert(data: { enum1: $enum1, enum2: $enum2 }) +} + +query MultipleEnumColumns_GetByKey($key: MultipleEnumColumns_Key!) @auth(level: PUBLIC) { + item: multipleEnumColumns(key: $key) { enum1 enum2 } +} diff --git a/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql b/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql index 4bad02dc62f..7baa331c408 100644 --- a/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql +++ b/firebase-dataconnect/emulator/dataconnect/schema/demo_schema.gql @@ -355,3 +355,91 @@ type OrderDirectionTest @table @index(fields: ["tag"]) { value: Int tag: String } + +enum N5ekmae3jn { + DPSKD6HR3A, + XGWGVMYTHJ, + QJX7C7RD5T, + RGTB44C2M8, + ZE6Z5778RV, + N3HWNCRWBP, +} + +type EnumNonNullable @table @index(fields: ["tag"]) { + value: N5ekmae3jn! + tag: String +} + +type EnumNonNullableTableDefault @table { + value: N5ekmae3jn! @default(value: RGTB44C2M8) +} + +type EnumNullable @table @index(fields: ["tag"]) { + value: N5ekmae3jn + tag: String +} + +type EnumNullableTableDefault @table { + value: N5ekmae3jn @default(value: ZE6Z5778RV) +} + +type EnumNonNullableListOfNonNullable @table { + value: [N5ekmae3jn!]! +} + +type EnumNonNullableListOfNullable @table { + value: [N5ekmae3jn]! +} + +type EnumNullableListOfNonNullable @table { + value: [N5ekmae3jn!] +} + +type EnumNullableListOfNullable @table { + value: [N5ekmae3jn] +} + +# An enum that uses Kotlin keywords to ensure they are correctly escaped. +enum break { + when, + super, + try, +} + +type EnumKotlinKeywords @table { + value: break! +} + +# An enum that has a subset of the values in the N5ekmae3jn enum. +# This is used for testing unknown enum values. +enum N5ekmae3jnSubset { + DPSKD6HR3A, + XGWGVMYTHJ, + QJX7C7RD5T, +} + +type EnumSubset @table { + value: N5ekmae3jnSubset! +} + +type EnumSubsetList @table { + value: [N5ekmae3jnSubset]! +} + +type EnumKey @table(key: ["id", "enumValue"]) { + id: UUID! @default(expr: "uuidV4()") + enumValue: N5ekmae3jn! + tag: String! +} + +enum S7yayynb25 { + XJ27ZAXKD3, + R36KQ8PT5K, + ETCV3FN9GH, + NMAJAGZHDS, +} + +type MultipleEnumColumns @table { + enum1: N5ekmae3jn! + enum2: S7yayynb25! +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/EnumIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/EnumIntegrationTest.kt new file mode 100644 index 00000000000..fcda65ec57d --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/EnumIntegrationTest.kt @@ -0,0 +1,888 @@ +/* + * 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. + */ + +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.withNullAppended +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.orNull +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import org.junit.Ignore +import org.junit.Test + +class EnumIntegrationTest : DataConnectIntegrationTestBase() { + + private val dataConnect: FirebaseDataConnect by lazy { + val connectorConfig = testConnectorConfig.copy(connector = "demo") + dataConnectFactory.newInstance(connectorConfig) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNonNullableNonNullEnumValue() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val insertVariables = InsertNonNullableVariables(enumValue) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe enumValue } + } + } + + @Test + fun insertNonNullableNullEnumValue() = runTest { + val insertVariables = InsertNonNullableNullVariables(null) + val mutation = dataConnect.mutation(insertVariables) + shouldThrow { mutation.execute() } + } + + @Test + fun updateNonNullableEnumValue() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, enumArb, enumArb) { value1, value2 -> + val insertVariables = InsertNonNullableVariables(value1) + val key = dataConnect.mutation(insertVariables).execute().data.key + val updateVariables = UpdateNonNullableVariables(key, value2) + dataConnect.mutation(updateVariables).execute() + val queryVariables = GetNonNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value2 } + } + } + + @Test + fun queryNonNullableEnumValue() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val insertVariables = InsertNonNullableVariables(enumValue) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableByKeyVariables(key) + val queryRef = dataConnect.query(queryVariables) + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe enumValue } + } + } + + @Test + fun queryNonNullableByNonNullEnumValue() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, enumArb, enumArb, enumArb, enumArb, Arb.dataConnect.tag()) { + value1, + value2, + value3, + value4, + tag -> + val insertVariables = Insert3NonNullableVariables(tag, value1, value2, value3) + val insertResult = dataConnect.mutation(insertVariables).execute().data + val queryVariables = GetNonNullableByTagAndValueVariables(tag, value4) + val queryResult = dataConnect.query(queryVariables).execute().data + val matchingKeys = insertResult.keysForMatchingValues(value4, insertVariables) + withClue(queryResult) { queryResult.items shouldContainExactlyInAnyOrder matchingKeys } + } + } + + @Test + fun queryNonNullableByUndefinedEnumValue() = runTest { + val enumArb = Arb.enum() + checkAll(1, enumArb, enumArb, enumArb, Arb.dataConnect.tag()) { value1, value2, value3, tag -> + val insertVariables = Insert3NonNullableVariables(tag, value1, value2, value3) + dataConnect.mutation(insertVariables).execute().data + val queryVariables = GetNonNullableByTagAndValueVariables(tag, OptionalVariable.Undefined) + val queryRef = dataConnect.query(queryVariables) + shouldThrow { queryRef.execute() } + } + } + + @Test + fun queryNonNullableByNullEnumValue() = runTest { + val enumArb = Arb.enum() + checkAll(1, enumArb, enumArb, enumArb, Arb.dataConnect.tag()) { value1, value2, value3, tag -> + val insertVariables = Insert3NonNullableVariables(tag, value1, value2, value3) + dataConnect.mutation(insertVariables).execute() + val queryVariables = GetNonNullableByTagAndValueVariables(tag, OptionalVariable.Value(null)) + val queryRef = dataConnect.query(queryVariables) + shouldThrow { queryRef.execute() } + } + } + + @Test + fun queryNonNullableByDefaultEnumValue() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, enumArb, enumArb, enumArb, Arb.dataConnect.tag()) { + value1, + value2, + value3, + tag -> + val insertVariables = Insert3NonNullableVariables(tag, value1, value2, value3) + val insertResult = dataConnect.mutation(insertVariables).execute().data + val queryVariables = GetNonNullableByTagAndDefaultValueVariables(tag) + val queryResult = dataConnect.query(queryVariables).execute().data + val matchingKeys = insertResult.keysForMatchingValues(N5ekmae3jn.XGWGVMYTHJ, insertVariables) + withClue(queryResult) { queryResult.items shouldContainExactlyInAnyOrder matchingKeys } + } + } + + @Serializable private data class InsertNonNullableVariables(val value: N5ekmae3jn) + + @Serializable private data class InsertNonNullableNullVariables(val value: N5ekmae3jn?) + + @Serializable + private data class Insert3NonNullableVariables( + val tag: String, + val value1: N5ekmae3jn, + val value2: N5ekmae3jn, + val value3: N5ekmae3jn + ) + + @Serializable private data class GetNonNullableByKeyVariables(val key: RowKey) + + @Serializable + private data class GetNonNullableByTagAndValueVariables( + val tag: String, + val value: OptionalVariable + ) { + constructor(tag: String, value: N5ekmae3jn) : this(tag, OptionalVariable.Value(value)) + } + + @Serializable private data class GetNonNullableByTagAndDefaultValueVariables(val tag: String) + + @Serializable + private data class UpdateNonNullableVariables(val key: RowKey, val value: N5ekmae3jn) + + @Serializable + private data class GetNonNullableByKeyData(val item: Item?) { + @Serializable data class Item(val value: N5ekmae3jn) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNullableNonNullEnumValue() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val insertVariables = InsertNullableVariables(enumValue) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe enumValue } + } + } + + @Test + fun insertNullableNullEnumValue() = runTest { + val insertVariables = InsertNullableVariables(null) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value.shouldBeNull() } + } + + @Test + fun updateNullableEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, enumArb, enumArb) { value1, value2 -> + val insertVariables = InsertNullableVariables(value1) + val key = dataConnect.mutation(insertVariables).execute().data.key + val updateVariables = UpdateNullableVariables(key, value2) + dataConnect.mutation(updateVariables).execute() + val queryVariables = GetNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value2 } + } + } + + @Test + fun queryNullableEnumValue() = runTest { + N5ekmae3jn.entries.withNullAppended().forEach { enumValue -> + val insertVariables = InsertNullableVariables(enumValue) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableByKeyVariables(key) + val queryRef = dataConnect.query(queryVariables) + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe enumValue } + } + } + + @Test + fun queryNullableByEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.33) + checkAll(NUM_ITERATIONS, enumArb, enumArb, enumArb, enumArb, Arb.dataConnect.tag()) { + value1, + value2, + value3, + value4, + tag -> + val insertVariables = Insert3NullableVariables(tag, value1, value2, value3) + val insertResult = dataConnect.mutation(insertVariables).execute().data + val queryVariables = GetNullableByTagAndValueVariables(tag, value4) + val queryResult = dataConnect.query(queryVariables).execute().data + val matchingKeys = insertResult.keysForMatchingValues(value4, insertVariables) + withClue(queryResult) { queryResult.items shouldContainExactlyInAnyOrder matchingKeys } + } + } + + @Test + fun queryNullableByUndefinedEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, enumArb, enumArb, enumArb, Arb.dataConnect.tag()) { + value1, + value2, + value3, + tag -> + val insertVariables = Insert3NullableVariables(tag, value1, value2, value3) + val insertResult = dataConnect.mutation(insertVariables).execute().data + val queryVariables = GetNullableByTagAndValueVariables(tag, OptionalVariable.Undefined) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult.items shouldContainExactlyInAnyOrder insertResult.keys } + } + } + + @Test + fun queryNullableByDefaultEnumValue() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.33) + checkAll(NUM_ITERATIONS, enumArb, enumArb, enumArb, Arb.dataConnect.tag()) { + value1, + value2, + value3, + tag -> + val insertVariables = Insert3NullableVariables(tag, value1, value2, value3) + val insertResult = dataConnect.mutation(insertVariables).execute().data + val queryVariables = GetNullableByTagAndDefaultValueVariables(tag) + val queryResult = dataConnect.query(queryVariables).execute().data + val matchingKeys = insertResult.keysForMatchingValues(N5ekmae3jn.QJX7C7RD5T, insertVariables) + withClue(queryResult) { queryResult.items shouldContainExactlyInAnyOrder matchingKeys } + } + } + + @Serializable private data class InsertNullableVariables(val value: N5ekmae3jn?) + + @Serializable + private data class Insert3NullableVariables( + val tag: String, + val value1: N5ekmae3jn?, + val value2: N5ekmae3jn?, + val value3: N5ekmae3jn? + ) + + @Serializable private data class GetNullableByKeyVariables(val key: RowKey) + + @Serializable + private data class GetNullableByTagAndValueVariables( + val tag: String, + val value: OptionalVariable + ) { + constructor(tag: String, value: N5ekmae3jn?) : this(tag, OptionalVariable.Value(value)) + } + + @Serializable private data class GetNullableByTagAndDefaultValueVariables(val tag: String) + + @Serializable private data class UpdateNullableVariables(val key: RowKey, val value: N5ekmae3jn?) + + @Serializable + private data class GetNullableByKeyData(val item: Item?) { + @Serializable data class Item(val value: N5ekmae3jn?) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullableTableDefault table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertEnumNonNullableTableDefault() = runTest { + val insertVariables = InsertNonNullableTableDefaultVariables + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableTableDefaultByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe N5ekmae3jn.RGTB44C2M8 } + } + + @Serializable private object InsertNonNullableTableDefaultVariables + + @Serializable private data class GetNonNullableTableDefaultByKeyVariables(val key: RowKey) + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullableTableDefault table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertEnumNullableTableDefault() = runTest { + val insertVariables = InsertNullableTableDefaultVariables + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableTableDefaultByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe N5ekmae3jn.ZE6Z5778RV } + } + + @Serializable private object InsertNullableTableDefaultVariables + + @Serializable private data class GetNullableTableDefaultByKeyVariables(val key: RowKey) + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullableListOfNonNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNonNullableListOfNonNullable_ListContainingNonNullValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { value -> + val insertVariables = InsertNonNullableListOfNonNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableListOfNonNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun insertNonNullableListOfNonNullable_NullList() = runTest { + val insertVariables = InsertNonNullableListOfNonNullableVariables(OptionalVariable.Value(null)) + val mutation = dataConnect.mutation(insertVariables) + shouldThrow { mutation.execute() } + } + + @Test + fun insertNonNullableListOfNonNullable_ListContainingNull() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..9).filter { it.contains(null) }) { value -> + val insertVariables = InsertNonNullableListOfNonNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableListOfNonNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun queryNonNullableListOfNonNullableEnumValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val insertVariables = InsertNonNullableListOfNonNullableVariables(values) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableListOfNonNullableByKeyVariables(key) + val queryRef = dataConnect.query(queryVariables) + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe values } + } + } + + @Serializable + private data class InsertNonNullableListOfNonNullableVariables( + val value: OptionalVariable?> + ) { + constructor(value: List) : this(OptionalVariable.Value(value)) + } + + @Serializable private data class GetNonNullableListOfNonNullableByKeyVariables(val key: RowKey) + + @Serializable + private data class GetNonNullableListOfNonNullableByKeyData(val item: Item?) { + @Serializable data class Item(val value: List) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNonNullableListOfNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNonNullableListOfNullable_ListNotContainingNull() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { value -> + val insertVariables = InsertNonNullableListOfNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableListOfNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun insertNonNullableListOfNullable_ListContainingNull() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..9).filter { it.contains(null) }) { value -> + val insertVariables = InsertNonNullableListOfNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableListOfNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun insertNonNullableListOfNullable_NullList() = runTest { + val insertVariables = InsertNonNullableListOfNullableVariables(OptionalVariable.Value(null)) + val mutation = dataConnect.mutation(insertVariables) + shouldThrow { mutation.execute() } + } + + @Test + fun queryNonNullableListOfNullableEnumValues() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val insertVariables = InsertNonNullableListOfNullableVariables(values) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNonNullableListOfNullableByKeyVariables(key) + val queryRef = dataConnect.query(queryVariables) + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe values } + } + } + + @Serializable + private data class InsertNonNullableListOfNullableVariables( + val value: OptionalVariable?> + ) { + constructor(value: List) : this(OptionalVariable.Value(value)) + } + + @Serializable private data class GetNonNullableListOfNullableByKeyVariables(val key: RowKey) + + @Serializable + private data class GetNonNullableListOfNullableByKeyData(val item: Item?) { + @Serializable data class Item(val value: List) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullableListOfNonNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNullableListOfNonNullable_ListContainingNonNullValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { value -> + val insertVariables = InsertNullableListOfNonNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNonNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun insertNullableListOfNonNullable_NullList() = runTest { + val insertVariables = InsertNullableListOfNonNullableVariables(null) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNonNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value.shouldBeNull() } + } + + @Test + fun insertNullableListOfNonNullable_ListContainingNull() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..9).filter { it.contains(null) }) { value -> + val insertVariables = InsertNullableListOfNonNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNonNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun queryNullableListOfNonNullableEnumValues() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val insertVariables = InsertNullableListOfNonNullableVariables(values) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNonNullableByKeyVariables(key) + val queryRef = dataConnect.query(queryVariables) + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe values } + } + } + + @Serializable + private data class InsertNullableListOfNonNullableVariables( + val value: OptionalVariable?> + ) { + constructor(value: List?) : this(OptionalVariable.Value(value)) + } + + @Serializable private data class GetNullableListOfNonNullableByKeyVariables(val key: RowKey) + + @Serializable + private data class GetNullableListOfNonNullableByKeyData(val item: Item?) { + @Serializable data class Item(val value: List?) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumNullableListOfNullable table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun insertNullableListOfNullable_ListContainingNonNullValues() = runTest { + val enumArb = Arb.enum() + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..5)) { value -> + val insertVariables = InsertNullableListOfNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun insertNullableListOfNullable_NullList() = runTest { + val insertVariables = InsertNullableListOfNullableVariables(null) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value.shouldBeNull() } + } + + @Test + fun insertNullableListOfNullable_ListContainingNull() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 0..9).filter { it.contains(null) }) { value -> + val insertVariables = InsertNullableListOfNullableVariables(value) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNullableByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe value } + } + } + + @Test + fun queryNullableListOfNullableEnumValues() = runTest { + val enumArb = Arb.enum().orNull(nullProbability = 0.5) + checkAll(NUM_ITERATIONS, Arb.list(enumArb, 10..20)) { values -> + val insertVariables = InsertNullableListOfNullableVariables(values) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetNullableListOfNullableByKeyVariables(key) + val queryRef = dataConnect.query(queryVariables) + val queryResult = queryRef.execute().data + withClue(queryResult) { queryResult?.item?.value shouldBe values } + } + } + + @Serializable + private data class InsertNullableListOfNullableVariables( + val value: OptionalVariable?> + ) { + constructor(value: List?) : this(OptionalVariable.Value(value)) + } + + @Serializable private data class GetNullableListOfNullableByKeyVariables(val key: RowKey) + + @Serializable + private data class GetNullableListOfNullableByKeyData(val item: Item?) { + @Serializable data class Item(val value: List?) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for EnumKey table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + @Ignore( + "TODO(b/432793533) Re-enable this test once the emulator crash " + + "caused by the \"EnumKey_GetByKey\" query is fixed." + ) + fun enumAsPrimaryKey() = runTest { + N5ekmae3jn.entries.forEach { enumValue -> + val tag = Arb.dataConnect.tag().next(rs) + val insertVariables = InsertEnumKeyVariables(enumValue, tag) + val key = dataConnect.mutation(insertVariables).execute().data.key + withClue(key) { key.enumValue shouldBe enumValue } + val queryVariables = GetEnumKeyByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { queryResult?.item?.tag shouldBe tag } + } + } + + @Serializable + private data class InsertEnumKeyVariables(val enumValue: N5ekmae3jn, val tag: String) + + @Serializable private data class InsertEnumKeyData(val key: EnumKeyKey) + + @Serializable private data class EnumKeyKey(val id: String, val enumValue: N5ekmae3jn) + + @Serializable private data class GetEnumKeyByKeyVariables(val key: EnumKeyKey) + + @Serializable + private data class GetEnumKeyByKeyData(val item: Item?) { + @Serializable data class Item(val tag: String) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for MultipleEnumColumns table. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun multipleEnumColumns() = runTest { + checkAll(NUM_ITERATIONS, Arb.enum(), Arb.enum()) { enum1, enum2 -> + val insertVariables = InsertMultipleEnumColumnsVariables(enum1, enum2) + val key = dataConnect.mutation(insertVariables).execute().data.key + val queryVariables = GetMultipleEnumColumnsByKeyVariables(key) + val queryResult = dataConnect.query(queryVariables).execute().data + withClue(queryResult) { + assertSoftly { + queryResult?.item?.enum1 shouldBe enum1 + queryResult?.item?.enum2 shouldBe enum2 + } + } + } + } + + @Serializable + private data class InsertMultipleEnumColumnsVariables( + val enum1: N5ekmae3jn, + val enum2: S7yayynb25 + ) + + @Serializable private data class GetMultipleEnumColumnsByKeyVariables(val key: RowKey) + + @Serializable + private data class GetMultipleEnumColumnsByKeyData(val item: Item?) { + @Serializable data class Item(val enum1: N5ekmae3jn, val enum2: S7yayynb25) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Helper classes and functions. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Serializable private data class RowKey(val id: String) + + @Serializable private data class QueryAllData(val items: List) + + @Serializable private data class InsertData(val key: RowKey) + + @Serializable + private data class Insert3Data(val key1: RowKey, val key2: RowKey, val key3: RowKey) { + + val keys: List + get() = listOf(key1, key2, key3) + + fun keysForMatchingValues( + value: N5ekmae3jn, + variables: Insert3NonNullableVariables + ): List = + keysForMatchingValues(value, variables.value1, variables.value2, variables.value3) + + fun keysForMatchingValues( + value: N5ekmae3jn?, + variables: Insert3NullableVariables + ): List = + keysForMatchingValues(value, variables.value1, variables.value2, variables.value3) + + private fun keysForMatchingValues( + value: N5ekmae3jn?, + value1: N5ekmae3jn?, + value2: N5ekmae3jn?, + value3: N5ekmae3jn?, + ): List = buildList { + if (value1 == value) { + add(key1) + } + if (value2 == value) { + add(key2) + } + if (value3 == value) { + add(key3) + } + } + } + + @Suppress("SpellCheckingInspection") + private enum class N5ekmae3jn { + DPSKD6HR3A, + XGWGVMYTHJ, + QJX7C7RD5T, + RGTB44C2M8, + ZE6Z5778RV, + N3HWNCRWBP, + } + + @Suppress("SpellCheckingInspection", "unused") + enum class S7yayynb25 { + XJ27ZAXKD3, + R36KQ8PT5K, + ETCV3FN9GH, + NMAJAGZHDS + } + + private companion object { + + /** The default number of iterations to use in property-based tests. */ + const val NUM_ITERATIONS = 10 + + fun FirebaseDataConnect.mutation( + variables: InsertNonNullableVariables + ): MutationRef = + mutation("EnumNonNullable_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNonNullableNullVariables + ): MutationRef = + mutation("EnumNonNullable_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNonNullableTableDefaultVariables + ): MutationRef = + mutation("EnumNonNullableTableDefault_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNullableTableDefaultVariables + ): MutationRef = + mutation("EnumNullableTableDefault_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNonNullableListOfNonNullableVariables + ): MutationRef = + mutation("EnumNonNullableListOfNonNullable_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNonNullableListOfNullableVariables + ): MutationRef = + mutation("EnumNonNullableListOfNullable_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNullableListOfNonNullableVariables + ): MutationRef = + mutation("EnumNullableListOfNonNullable_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNullableListOfNullableVariables + ): MutationRef = + mutation("EnumNullableListOfNullable_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: Insert3NonNullableVariables + ): MutationRef = + mutation("EnumNonNullable_Insert3", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: UpdateNonNullableVariables + ): MutationRef = + mutation("EnumNonNullable_UpdateByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertEnumKeyVariables + ): MutationRef = + mutation("EnumKey_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertMultipleEnumColumnsVariables + ): MutationRef = + mutation("MultipleEnumColumns_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNonNullableByKeyVariables + ): QueryRef = + query("EnumNonNullable_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNonNullableListOfNonNullableByKeyVariables + ): QueryRef< + GetNonNullableListOfNonNullableByKeyData?, GetNonNullableListOfNonNullableByKeyVariables + > = query("EnumNonNullableListOfNonNullable_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNonNullableListOfNullableByKeyVariables + ): QueryRef< + GetNonNullableListOfNullableByKeyData?, GetNonNullableListOfNullableByKeyVariables + > = query("EnumNonNullableListOfNullable_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNullableListOfNonNullableByKeyVariables + ): QueryRef< + GetNullableListOfNonNullableByKeyData?, GetNullableListOfNonNullableByKeyVariables + > = query("EnumNullableListOfNonNullable_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNullableListOfNullableByKeyVariables + ): QueryRef = + query("EnumNullableListOfNullable_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNonNullableByTagAndValueVariables + ): QueryRef = + query("EnumNonNullable_GetAllByTagAndValue", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNonNullableByTagAndDefaultValueVariables + ): QueryRef = + query("EnumNonNullable_GetAllByTagAndDefaultValue", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetEnumKeyByKeyVariables + ): QueryRef = + query("EnumKey_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetMultipleEnumColumnsByKeyVariables + ): QueryRef = + query("MultipleEnumColumns_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: InsertNullableVariables + ): MutationRef = + mutation("EnumNullable_Insert", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: Insert3NullableVariables + ): MutationRef = + mutation("EnumNullable_Insert3", variables, serializer(), serializer()) + + fun FirebaseDataConnect.mutation( + variables: UpdateNullableVariables + ): MutationRef = + mutation("EnumNullable_UpdateByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNullableByKeyVariables + ): QueryRef = + query("EnumNullable_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNonNullableTableDefaultByKeyVariables + ): QueryRef = + query("EnumNonNullableTableDefault_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNullableTableDefaultByKeyVariables + ): QueryRef = + query("EnumNullableTableDefault_GetByKey", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNullableByTagAndValueVariables + ): QueryRef = + query("EnumNullable_GetAllByTagAndValue", variables, serializer(), serializer()) + + fun FirebaseDataConnect.query( + variables: GetNullableByTagAndDefaultValueVariables + ): QueryRef = + query("EnumNullable_GetAllByTagAndDefaultValue", variables, serializer(), serializer()) + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/WithNullAppended.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/WithNullAppended.kt new file mode 100644 index 00000000000..c6dc53cc0e1 --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/WithNullAppended.kt @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package com.google.firebase.dataconnect.testutil + +/** + * Creates and returns a new [List] that contains all of the elements in the receiving [List] but + * with a `null` element added to the end. + */ +@JvmName("withNullAppendedCollection") +fun Collection.withNullAppended(): List = + buildList(size + 1) { + addAll(this@withNullAppended) + add(null) + } + +/** + * Creates and returns a new [List] that contains all of the elements in the receiving [Iterable] + * but with a `null` element added to the end. + */ +@JvmName("withNullAppendedIterable") +fun Iterable.withNullAppended(): List = buildList { + addAll(this@withNullAppended) + add(null) +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt index c49271abade..61703b784f5 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt @@ -18,8 +18,17 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary import io.kotest.property.Arb import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.bind +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.flatMap +import io.kotest.property.arbitrary.map import kotlin.random.nextInt +/** Returns a new [Arb] that produces two _unequal_ values of this [Arb]. */ +fun Arb.distinctPair(): Arb> = flatMap { value1 -> + this@distinctPair.filter { it != value1 }.map { Pair(value1, it) } +} + fun Arb.withPrefix(prefix: String): Arb = arbitrary { "$prefix${bind()}" } fun Arb.Companion.positiveIntWithUniformNumDigitsProbability(range: IntRange): Arb { diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt new file mode 100644 index 00000000000..074b5f74953 --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package com.google.firebase.dataconnect.testutil.property.arbitrary + +import io.kotest.property.Arb +import io.kotest.property.arbitrary.bind + +data class TwoValues(val value1: T, val value2: T) + +data class ThreeValues(val value1: T, val value2: T, val value3: T) + +data class FourValues(val value1: T, val value2: T, val value3: T, val value4: T) + +data class FiveValues(val value1: T, val value2: T, val value3: T, val value4: T, val value5: T) + +fun Arb.Companion.twoValues(arb: Arb): Arb> = + bind(arb, arb) { value1, value2 -> TwoValues(value1, value2) } + +fun Arb.Companion.threeValues(arb: Arb): Arb> = + bind(arb, arb, arb) { value1, value2, value3 -> ThreeValues(value1, value2, value3) } + +fun Arb.Companion.fourValues(arb: Arb): Arb> = + bind(arb, arb, arb, arb) { value1, value2, value3, value4 -> + FourValues(value1, value2, value3, value4) + } + +fun Arb.Companion.fiveValues(arb: Arb): Arb> = + bind(arb, arb, arb, arb, arb) { value1, value2, value3, value4, value5 -> + FiveValues(value1, value2, value3, value4, value5) + } diff --git a/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/WithNullAppendedUnitTest.kt b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/WithNullAppendedUnitTest.kt new file mode 100644 index 00000000000..b4f026c857a --- /dev/null +++ b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/WithNullAppendedUnitTest.kt @@ -0,0 +1,63 @@ +/* + * 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. + */ + +package com.google.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.list +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class WithNullAppendedUnitTest { + + @Test + fun `Iterable withNullAppended on an empty iterable`() { + val iterable: Iterable = emptyList() + iterable.withNullAppended() shouldBe listOf(null) + } + + @Test + fun `Iterable withNullAppended on a non-empty iterable`() = runTest { + checkAll(NUM_ITERATIONS, Arb.list(Arb.dataConnect.string(), 1..100)) { list -> + val iterable: Iterable = list + val expected = List(list.size + 1) { if (it < list.size) list[it] else null } + iterable.withNullAppended() shouldBe expected + } + } + + @Test + fun `Collection withNullAppended on an empty collection`() { + val collection: Collection = emptyList() + collection.withNullAppended() shouldBe listOf(null) + } + + @Test + fun `Collection withNullAppended on a non-empty collection`() = runTest { + checkAll(NUM_ITERATIONS, Arb.list(Arb.dataConnect.string(), 1..100)) { list -> + val collection: Collection = list + val expected = List(list.size + 1) { if (it < list.size) list[it] else null } + collection.withNullAppended() shouldBe expected + } + } + + private companion object { + + const val NUM_ITERATIONS = 100 + } +}