diff --git a/firestore/src/androidTest/kotlin/de/sipgate/federmappe/firestore/integration/FirestoreIntegrationTest.kt b/firestore/src/androidTest/kotlin/de/sipgate/federmappe/firestore/integration/FirestoreIntegrationTest.kt index 261504e..a38702c 100644 --- a/firestore/src/androidTest/kotlin/de/sipgate/federmappe/firestore/integration/FirestoreIntegrationTest.kt +++ b/firestore/src/androidTest/kotlin/de/sipgate/federmappe/firestore/integration/FirestoreIntegrationTest.kt @@ -50,10 +50,13 @@ class FirestoreIntegrationTest { @JvmStatic fun initializeTestData(): Unit = runTest { val users = firestore.collection("$tempNamespace-users") - val root = users.add(mapOf( - "name" to "root", - "createdAt" to Timestamp(1720794610L, 0), - )).await() + val root = users.add( + mapOf( + "name" to "root", + "createdAt" to Timestamp(1720794610L, 0), + "nested" to mapOf("updatedAt" to Timestamp(1720794610L, 0)) + ) + ).await() root.update("id", root.id).await() } } @@ -100,6 +103,6 @@ class FirestoreIntegrationTest { } } assertTrue(b.isNotEmpty()) - assertIs< Entity.FullUser>(b.first()) + assertIs(b.first()) } } diff --git a/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt b/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt index 24406dc..e39c046 100644 --- a/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt +++ b/firestore/src/main/java/de/sipgate/federmappe/firestore/QuerySnapshotExt.kt @@ -25,16 +25,20 @@ inline fun QuerySnapshot.toObjects( } } +@Suppress("UNCHECKED_CAST") fun Map.normalizeStringMap(): Map = mapValues { when (val value = it.value) { is Timestamp -> value.toDecodableTimestamp() + is Map<*, *> -> (value as? Map)?.normalizeStringMap() ?: value else -> value } } +@Suppress("UNCHECKED_CAST") fun Map.normalizeStringMapNullable(): Map = mapValues { when (val value = it.value) { is Timestamp -> value.toDecodableTimestamp() + is Map<*, *> -> (value as? Map)?.normalizeStringMap() ?: value else -> value } } diff --git a/firestore/src/test/kotlin/de/sipgate/federmappe/firestore/types/FirestoreTimestampToDecodableTimestampTest.kt b/firestore/src/test/kotlin/de/sipgate/federmappe/firestore/types/FirestoreTimestampToDecodableTimestampTest.kt index ae0f257..f530a3c 100644 --- a/firestore/src/test/kotlin/de/sipgate/federmappe/firestore/types/FirestoreTimestampToDecodableTimestampTest.kt +++ b/firestore/src/test/kotlin/de/sipgate/federmappe/firestore/types/FirestoreTimestampToDecodableTimestampTest.kt @@ -2,6 +2,8 @@ package de.sipgate.federmappe.firestore.types import com.google.firebase.Timestamp import de.sipgate.federmappe.common.decoder.StringMapToObjectDecoder +import de.sipgate.federmappe.firestore.normalizeStringMap +import de.sipgate.federmappe.firestore.normalizeStringMapNullable import kotlinx.datetime.Instant import kotlinx.datetime.serializers.InstantComponentSerializer import kotlinx.datetime.toJavaInstant @@ -137,4 +139,82 @@ class FirestoreTimestampToDecodableTimestampTest { assertEquals(null, result.createdAt) assertIs(result) } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun nestedFirestoreTimestampIsNormalizedAndDecodedToKotlinInstantCorrectly() { + // Arrange + val expectedInstant = Instant.fromEpochSeconds(1716823455) + val expectedDate = Date.from(expectedInstant.toJavaInstant()) + val timestamp = Timestamp(expectedDate) + + @Serializable + data class MockNestedDataClass( + @Contextual + val createdAt: Instant + ) + + @Serializable + data class MockLocalDataClass( + @Contextual + val nested: MockNestedDataClass + ) + + val serializer = serializer() + + val data = + mapOf("nested" to mapOf("createdAt" to timestamp)).normalizeStringMap() + + // Act + val result = + serializer.deserialize( + StringMapToObjectDecoder( + data = data, + serializersModule = SerializersModule { contextual(InstantComponentSerializer) }, + ), + ) + + // Assert + assertEquals(expectedInstant, result.nested.createdAt) + assertIs(result) + } + + @OptIn(ExperimentalSerializationApi::class) + @Test + fun nestedFirestoreTimestampIsNormalizedAndDecodedToKotlinInstantCorrectlyNullable() { + // Arrange + val expectedInstant = Instant.fromEpochSeconds(1716823455) + val expectedDate = Date.from(expectedInstant.toJavaInstant()) + val timestamp = Timestamp(expectedDate) + + @Serializable + data class MockNestedDataClass( + @Contextual + val createdAt: Instant + ) + + @Serializable + data class MockLocalDataClass( + @Contextual + val nested: MockNestedDataClass + ) + + val serializer = serializer() + + val data = + mapOf("nested" to mapOf("createdAt" to timestamp)).normalizeStringMapNullable() + + // Act + val result = + serializer.deserialize( + StringMapToObjectDecoder( + data = data, + serializersModule = SerializersModule { contextual(InstantComponentSerializer) }, + ), + ) + + // Assert + assertEquals(expectedInstant, result.nested.createdAt) + assertIs(result) + } }