diff --git a/README.md b/README.md index 842a36cb7..675f6e2e0 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,15 @@ citiesRef.where { } ``` +Similar methods exist for `update` methods in the Firestore module: + +```kotlin +documentRef.update { + "field" to "value" + "otherField".to(IntAsStringSerializer(), 1) +} +``` +

Operator overloading

In cases where it makes sense, such as Firebase Functions HTTPS Callable, operator overloading is used: diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceTest.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceTest.kt index 446dbb4ab..d58024788 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceTest.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/DocumentReferenceTest.kt @@ -2,6 +2,7 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.internal.decode import dev.gitlive.firebase.runTest +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -90,10 +91,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testServerTimestampFieldValue() = runTest { - val doc = firestore + fun testServerTimestampFieldValue() = testDocument( + firestore .collection("testServerTimestampFieldValue") - .document("test") + .document("test"), + ) { doc -> doc.set( FirestoreTimeTest.serializer(), FirestoreTimeTest("ServerTimestamp", Timestamp(123, 0)), @@ -107,11 +109,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testServerTimestampBehaviorNone() = runTest { - val doc = firestore + fun testServerTimestampBehaviorNone() = testDocument( + firestore .collection("testServerTimestampBehaviorNone") - .document("test${Random.nextInt()}") - + .document("test${Random.nextInt()}"), + ) { doc -> val deferredPendingWritesSnapshot = async { doc.snapshots.filter { it.exists }.first() } @@ -128,11 +130,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testServerTimestampBehaviorEstimate() = runTest { - val doc = firestore + fun testServerTimestampBehaviorEstimate() = testDocument( + firestore .collection("testServerTimestampBehaviorEstimate") - .document("test${Random.nextInt()}") - + .document("test${Random.nextInt()}"), + ) { doc -> val deferredPendingWritesSnapshot = async { doc.snapshots.filter { it.exists }.first() } @@ -147,11 +149,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testServerTimestampBehaviorPrevious() = runTest { - val doc = firestore + fun testServerTimestampBehaviorPrevious() = testDocument( + firestore .collection("testServerTimestampBehaviorPrevious") - .document("test${Random.nextInt()}") - + .document("test${Random.nextInt()}"), + ) { doc -> val deferredPendingWritesSnapshot = async { doc.snapshots.filter { it.exists }.first() } @@ -165,10 +167,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testDocumentAutoId() = runTest { - val doc = firestore + fun testDocumentAutoId() = testDocument( + firestore .collection("testDocumentAutoId") - .document + .document, + ) { doc -> doc.set(FirestoreTest.serializer(), FirestoreTest("AutoId")) @@ -182,12 +185,20 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testUpdateValues() = runTest { - val doc = firestore + fun testUpdateValues() = testDocument( + firestore .collection("testFirestoreUpdateMultipleValues") - .document("test1") - - doc.set(FirestoreTest.serializer(), FirestoreTest("property", count = 0, nested = NestedObject("nested"), duration = 600.milliseconds)) + .document("test1"), + ) { doc -> + doc.set( + FirestoreTest.serializer(), + FirestoreTest( + "property", + count = 0, + nested = NestedObject("nested"), + duration = 600.milliseconds, + ), + ) val dataBefore = doc.get().data(FirestoreTest.serializer()) assertEquals(0, dataBefore.count) assertNull(dataBefore.optional) @@ -197,8 +208,14 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { doc.update { FirestoreTest::count.name to 5 FieldPath(FirestoreTest::optional.name) to "notNull" - FirestoreTest::nested.name.to(NestedObject.serializer(), NestedObject("newProperty")) - FieldPath(FirestoreTest::duration.name).to(DurationAsLongSerializer(), 700.milliseconds) + FirestoreTest::nested.name.to( + NestedObject.serializer(), + NestedObject("newProperty"), + ) + FieldPath(FirestoreTest::duration.name).to( + DurationAsIntSerializer(), + 700.milliseconds, + ) } val dataAfter = doc.get().data(FirestoreTest.serializer()) assertEquals(5, dataAfter.count) @@ -208,11 +225,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testIncrementFieldValue() = runTest { - val doc = firestore + fun testIncrementFieldValue() = testDocument( + firestore .collection("testFirestoreIncrementFieldValue") - .document("test1") - + .document("test1"), + ) { doc -> doc.set(FirestoreTest.serializer(), FirestoreTest("increment1", count = 0)) val dataBefore = doc.get().data(FirestoreTest.serializer()) assertEquals(0, dataBefore.count) @@ -223,11 +240,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testArrayUnion() = runTest { - val doc = firestore + fun testArrayUnion() = testDocument( + firestore .collection("testFirestoreArrayUnion") - .document("test1") - + .document("test1"), + ) { doc -> doc.set(FirestoreTest.serializer(), FirestoreTest("increment1", list = listOf("first"))) val dataBefore = doc.get().data(FirestoreTest.serializer()) assertEquals(listOf("first"), dataBefore.list) @@ -238,11 +255,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testArrayRemove() = runTest { - val doc = firestore + fun testArrayRemove() = testDocument( + firestore .collection("testFirestoreArrayRemove") - .document("test1") - + .document("test1"), + ) { doc -> doc.set(FirestoreTest.serializer(), FirestoreTest("increment1", list = listOf("first", "second"))) val dataBefore = doc.get().data(FirestoreTest.serializer()) assertEquals(listOf("first", "second"), dataBefore.list) @@ -253,17 +270,17 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testLegacyDoubleTimestamp() = runTest { + fun testLegacyDoubleTimestamp() = testDocument( + firestore + .collection("testLegacyDoubleTimestamp") + .document("test${Random.nextInt()}"), + ) { doc -> @Serializable data class DoubleTimestamp( @Serializable(with = DoubleAsTimestampSerializer::class) val time: Double?, ) - val doc = firestore - .collection("testLegacyDoubleTimestamp") - .document("test${Random.nextInt()}") - val deferredPendingWritesSnapshot = async { doc.snapshots.filter { it.exists }.first() } @@ -278,7 +295,11 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testLegacyDoubleTimestampWriteNewFormatRead() = runTest { + fun testLegacyDoubleTimestampWriteNewFormatRead() = testDocument( + firestore + .collection("testLegacyDoubleTimestampEncodeDecode") + .document("testLegacy"), + ) { doc -> @Serializable data class LegacyDocument( @Serializable(with = DoubleAsTimestampSerializer::class) @@ -290,10 +311,6 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { val time: Timestamp, ) - val doc = firestore - .collection("testLegacyDoubleTimestampEncodeDecode") - .document("testLegacy") - val ms = 12345678.0 doc.set(LegacyDocument.serializer(), LegacyDocument(time = ms)) @@ -303,25 +320,25 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { } @Test - fun testGeoPointSerialization() = runTest { + fun testGeoPointSerialization() = testDocument( + firestore.collection("geoPointSerialization") + .document("geoPointSerialization"), + ) { doc -> @Serializable data class DataWithGeoPoint(val geoPoint: GeoPoint) - fun getDocument() = firestore.collection("geoPointSerialization") - .document("geoPointSerialization") - val data = DataWithGeoPoint(GeoPoint(12.34, 56.78)) // store geo point - getDocument().set(DataWithGeoPoint.serializer(), data) + doc.set(DataWithGeoPoint.serializer(), data) // restore data - val savedData = getDocument().get().data(DataWithGeoPoint.serializer()) + val savedData = doc.get().data(DataWithGeoPoint.serializer()) assertEquals(data.geoPoint, savedData.geoPoint) // update data val updatedData = DataWithGeoPoint(GeoPoint(87.65, 43.21)) - getDocument().update(FieldPath(DataWithGeoPoint::geoPoint.name) to updatedData.geoPoint) + doc.update(FieldPath(DataWithGeoPoint::geoPoint.name) to updatedData.geoPoint) // verify update - val updatedSavedData = getDocument().get().data(DataWithGeoPoint.serializer()) + val updatedSavedData = doc.get().data(DataWithGeoPoint.serializer()) assertEquals(updatedData.geoPoint, updatedSavedData.geoPoint) } @@ -332,66 +349,82 @@ class DocumentReferenceTest : BaseFirebaseFirestoreTest() { val documentReference: DocumentReference, ) - fun getCollection() = firestore.collection("documentReferenceSerialization") - fun getDocument() = getCollection() + val collection = firestore.collection("documentReferenceSerialization") + val document = collection .document("documentReferenceSerialization") - val documentRef1 = getCollection().document("refDoc1").apply { - set(mapOf("value" to 1)) - } - val documentRef2 = getCollection().document("refDoc2").apply { - set(mapOf("value" to 2)) - } - - val data = DataWithDocumentReference(documentRef1) - // store reference - getDocument().set(DataWithDocumentReference.serializer(), data) - // restore data - val savedData = getDocument().get().data(DataWithDocumentReference.serializer()) - assertEquals(data.documentReference.path, savedData.documentReference.path) - - // update data - val updatedData = DataWithDocumentReference(documentRef2) - getDocument().update { - FieldPath(DataWithDocumentReference::documentReference.name).to( - DocumentReferenceSerializer, - updatedData.documentReference, + val documentRef1 = collection.document("refDoc1") + val documentRef2 = collection.document("refDoc2") + + try { + documentRef1.set(mapOf("value" to 1)) + documentRef2.set(mapOf("value" to 2)) + val data = DataWithDocumentReference(documentRef1) + // store reference + document.set(DataWithDocumentReference.serializer(), data) + // restore data + val savedData = document.get().data(DataWithDocumentReference.serializer()) + assertEquals(data.documentReference.path, savedData.documentReference.path) + + // update data + val updatedData = DataWithDocumentReference(documentRef2) + document.update { + FieldPath(DataWithDocumentReference::documentReference.name).to( + DocumentReferenceSerializer, + updatedData.documentReference, + ) + } + // verify update + val updatedSavedData = document.get().data(DataWithDocumentReference.serializer()) + assertEquals( + updatedData.documentReference.path, + updatedSavedData.documentReference.path, ) + } finally { + document.delete() + documentRef1.delete() + documentRef2.delete() } - // verify update - val updatedSavedData = getDocument().get().data(DataWithDocumentReference.serializer()) - assertEquals(updatedData.documentReference.path, updatedSavedData.documentReference.path) } @Test - fun testFieldValuesOps() = runTest { + fun testFieldValuesOps() = testDocument( + firestore.collection("fieldValuesOps") + .document("fieldValuesOps"), + ) { doc -> @Serializable data class TestData(val values: List) - fun getDocument() = firestore.collection("fieldValuesOps") - .document("fieldValuesOps") val data = TestData(listOf(1)) // store - getDocument().set(TestData.serializer(), data) + doc.set(TestData.serializer(), data) // append & verify - getDocument().update(FieldPath(TestData::values.name) to FieldValue.arrayUnion(2)) + doc.update(FieldPath(TestData::values.name) to FieldValue.arrayUnion(2)) - var savedData = getDocument().get().data(TestData.serializer()) + var savedData = doc.get().data(TestData.serializer()) assertEquals(listOf(1, 2), savedData.values) // remove & verify - getDocument().update(FieldPath(TestData::values.name) to FieldValue.arrayRemove(1)) - savedData = getDocument().get().data(TestData.serializer()) + doc.update(FieldPath(TestData::values.name) to FieldValue.arrayRemove(1)) + savedData = doc.get().data(TestData.serializer()) assertEquals(listOf(2), savedData.values) - val list = getDocument().get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) + val list = doc.get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) assertEquals(listOf(2), list) // delete & verify - getDocument().update(FieldPath(TestData::values.name) to FieldValue.delete) - val deletedList = getDocument().get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) + doc.update(FieldPath(TestData::values.name) to FieldValue.delete) + val deletedList = doc.get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) assertNull(deletedList) } private suspend fun nonSkippedDelay(timeout: Duration) = withContext(Dispatchers.Default) { delay(timeout) } + + private fun testDocument(document: DocumentReference, block: suspend CoroutineScope.(DocumentReference) -> Unit) = runTest { + try { + block(document) + } finally { + document.delete() + } + } } diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt index ecc5b386b..f87664321 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt @@ -21,10 +21,6 @@ class FirestoreSourceTest { ) } - private suspend fun setDoc() { - firestore.collection("testFirestoreQuerying").document("one").set(testDoc) - } - private fun initializeFirebase(persistenceEnabled: Boolean = false) { val app = Firebase.apps(context).firstOrNull() ?: Firebase.initialize( context, @@ -58,67 +54,75 @@ class FirestoreSourceTest { } @Test - fun testGetFromServer_withPersistence() = runTest { - initializeFirebase(persistenceEnabled = true) - setDoc() - val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.SERVER) + fun testGetFromServer_withPersistence() = testFirebaseDoc(true) { + val doc = get(Source.SERVER) assertTrue(doc.exists) assertFalse(doc.metadata.isFromCache) } @Test - fun testGetFromServer_withoutPersistence() = runTest { - initializeFirebase(persistenceEnabled = false) - setDoc() - val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.SERVER) + fun testGetFromServer_withoutPersistence() = testFirebaseDoc(false) { + val doc = get(Source.SERVER) assertTrue(doc.exists) assertFalse(doc.metadata.isFromCache) } @Test - fun testGetFromCache() = runTest { - initializeFirebase(persistenceEnabled = true) - + fun testGetFromCache() = testFirebaseDoc(true) { // Warm up cache by setting a document - setDoc() + set(testDoc) - val cachedDoc = firestore.collection("testFirestoreQuerying").document("one").get(Source.CACHE) + val cachedDoc = get(Source.CACHE) assertTrue(cachedDoc.exists) assertTrue(cachedDoc.metadata.isFromCache) } @Test - fun testGetFromCache_withoutPersistence() = runTest { - initializeFirebase(persistenceEnabled = false) - setDoc() + fun testGetFromCache_withoutPersistence() = testFirebaseDoc(false) { assertFailsWith(FirebaseFirestoreException::class) { - firestore.collection("testFirestoreQuerying").document("one").get(Source.CACHE) + get(Source.CACHE) } } @Test - fun testGetDefault_withPersistence() = runTest { - initializeFirebase(persistenceEnabled = false) - val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.DEFAULT) + fun testGetDefault_withPersistence() = testFirebaseDoc(false) { + val doc = get(Source.DEFAULT) assertTrue(doc.exists) assertFalse(doc.metadata.isFromCache) } @Test - fun testGet() = runTest { - initializeFirebase(persistenceEnabled = false) - val doc = firestore.collection("testFirestoreQuerying").document("one").get() + fun testGet() = testFirebaseDoc(false) { + val doc = get() assertTrue(doc.exists) assertFalse(doc.metadata.isFromCache) } @Test - fun testGetDefault_withoutPersistence() = runTest { - initializeFirebase(persistenceEnabled = true) - setDoc() - val doc = firestore.collection("testFirestoreQuerying").document("one").get(Source.DEFAULT) + fun testGetDefault_withoutPersistence() = testFirebaseDoc(true) { + val doc = get(Source.DEFAULT) assertTrue(doc.exists) // Firebase defaults to first fetching from server assertFalse(doc.metadata.isFromCache) } + + private fun testFirebaseDoc( + persistenceEnabled: Boolean, + block: suspend DocumentReference.() -> Unit, + ) = runTest { + initializeFirebase() + val doc = firestore.collection("testFirestoreQuerying").document("one") + doc.set(testDoc) + + Firebase.apps(context).forEach { it.delete() } + + initializeFirebase(persistenceEnabled = persistenceEnabled) + + val newDoc = firestore.collection("testFirestoreQuerying").document("one") + try { + newDoc.block() + } finally { + newDoc.delete() + } + } } diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/QueryTest.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/QueryTest.kt index 9d25c5102..665044bda 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/QueryTest.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/QueryTest.kt @@ -177,12 +177,12 @@ open class QueryTest : BaseFirebaseFirestoreTest() { pathQuery.assertDocuments(FirestoreTest.serializer(), testOne) val serializeFieldQuery = collection.where { - "duration".lessThan(DurationAsLongSerializer(), testThree.duration) + "duration".lessThan(DurationAsIntSerializer(), testThree.duration) } serializeFieldQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo) val serializePathQuery = collection.where { - FieldPath(FirestoreTest::duration.name).lessThan(DurationAsLongSerializer(), testTwo.duration) + FieldPath(FirestoreTest::duration.name).lessThan(DurationAsIntSerializer(), testTwo.duration) } serializePathQuery.assertDocuments(FirestoreTest.serializer(), testOne) } @@ -200,12 +200,12 @@ open class QueryTest : BaseFirebaseFirestoreTest() { pathQuery.assertDocuments(FirestoreTest.serializer(), testThree) val serializeFieldQuery = collection.where { - "duration".greaterThan(DurationAsLongSerializer(), testOne.duration) + "duration".greaterThan(DurationAsIntSerializer(), testOne.duration) } serializeFieldQuery.assertDocuments(FirestoreTest.serializer(), testTwo, testThree) val serializePathQuery = collection.where { - FieldPath(FirestoreTest::duration.name).greaterThan(DurationAsLongSerializer(), testTwo.duration) + FieldPath(FirestoreTest::duration.name).greaterThan(DurationAsIntSerializer(), testTwo.duration) } serializePathQuery.assertDocuments(FirestoreTest.serializer(), testThree) } @@ -223,12 +223,12 @@ open class QueryTest : BaseFirebaseFirestoreTest() { pathQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo) val serializeFieldQuery = collection.where { - "duration".lessThanOrEqualTo(DurationAsLongSerializer(), testOne.duration) + "duration".lessThanOrEqualTo(DurationAsIntSerializer(), testOne.duration) } serializeFieldQuery.assertDocuments(FirestoreTest.serializer(), testOne) val serializePathQuery = collection.where { - FieldPath(FirestoreTest::duration.name).lessThanOrEqualTo(DurationAsLongSerializer(), testTwo.duration) + FieldPath(FirestoreTest::duration.name).lessThanOrEqualTo(DurationAsIntSerializer(), testTwo.duration) } serializePathQuery.assertDocuments(FirestoreTest.serializer(), testOne, testTwo) } @@ -246,12 +246,12 @@ open class QueryTest : BaseFirebaseFirestoreTest() { pathQuery.assertDocuments(FirestoreTest.serializer(), testTwo, testThree) val serializeFieldQuery = collection.where { - "duration".greaterThanOrEqualTo(DurationAsLongSerializer(), testTwo.duration) + "duration".greaterThanOrEqualTo(DurationAsIntSerializer(), testTwo.duration) } serializeFieldQuery.assertDocuments(FirestoreTest.serializer(), testTwo, testThree) val serializePathQuery = collection.where { - FieldPath(FirestoreTest::duration.name).greaterThanOrEqualTo(DurationAsLongSerializer(), testThree.duration) + FieldPath(FirestoreTest::duration.name).greaterThanOrEqualTo(DurationAsIntSerializer(), testThree.duration) } serializePathQuery.assertDocuments(FirestoreTest.serializer(), testThree) } @@ -397,20 +397,25 @@ open class QueryTest : BaseFirebaseFirestoreTest() { val pastTimestamp = Timestamp(timestamp.seconds - 60, 12345000) // note: iOS truncates 3 last digits of nanoseconds due to internal conversions val futureTimestamp = Timestamp(timestamp.seconds + 60, 78910000) - collection.add(DocumentWithTimestamp.serializer(), DocumentWithTimestamp(pastTimestamp)) - collection.add(DocumentWithTimestamp.serializer(), DocumentWithTimestamp(futureTimestamp)) + val doc1 = collection.add(DocumentWithTimestamp.serializer(), DocumentWithTimestamp(pastTimestamp)) + val doc2 = collection.add(DocumentWithTimestamp.serializer(), DocumentWithTimestamp(futureTimestamp)) - val equalityQueryResult = collection.where { - FieldPath(DocumentWithTimestamp::time.name) equalTo pastTimestamp - }.get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet() + try { + val equalityQueryResult = collection.where { + FieldPath(DocumentWithTimestamp::time.name) equalTo pastTimestamp + }.get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet() - assertEquals(setOf(DocumentWithTimestamp(pastTimestamp)), equalityQueryResult) + assertEquals(setOf(DocumentWithTimestamp(pastTimestamp)), equalityQueryResult) - val gtQueryResult = collection.where { - FieldPath(DocumentWithTimestamp::time.name) greaterThan timestamp - }.get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet() + val gtQueryResult = collection.where { + FieldPath(DocumentWithTimestamp::time.name) greaterThan timestamp + }.get().documents.map { it.data(DocumentWithTimestamp.serializer()) }.toSet() - assertEquals(setOf(DocumentWithTimestamp(futureTimestamp)), gtQueryResult) + assertEquals(setOf(DocumentWithTimestamp(futureTimestamp)), gtQueryResult) + } finally { + doc1.delete() + doc2.delete() + } } @Test diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TransactionTest.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TransactionTest.kt new file mode 100644 index 000000000..5a68d53a8 --- /dev/null +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/TransactionTest.kt @@ -0,0 +1,53 @@ +package dev.gitlive.firebase.firestore + +import dev.gitlive.firebase.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds + +@IgnoreForAndroidUnitTest +class TransactionTest : BaseFirebaseFirestoreTest() { + + @Test + fun runTransaction() = runTest { + val collection = firestore.collection("testServerTestTransaction") + val document = collection.document("doc1") + try { + document.set( + strategy = FirestoreTest.serializer(), + data = FirestoreTest( + prop1 = "prop1", + count = 0, + ), + ) + val result = firestore.runTransaction { + val count = get(document).data(FirestoreTest.serializer()).count + + if (count < 1) { + update(document) { + FirestoreTest::prop1.name to "newProperty" + FieldPath(FirestoreTest::count.name) to 5 + FirestoreTest::duration.name.to(DurationAsIntSerializer(), 100.milliseconds) + FieldPath(FirestoreTest::nested.name).to( + NestedObject.serializer(), + NestedObject("nested"), + ) + } + true + } else { + throw IllegalStateException("Invalid count") + } + } + assertTrue(result) + + val updated = document.get().data(FirestoreTest.serializer()) + assertEquals("newProperty", updated.prop1) + assertEquals(5, updated.count) + assertEquals(100.milliseconds, updated.duration) + assertEquals(NestedObject("nested"), updated.nested) + } finally { + document.delete() + } + } +} diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/WriteBatchTest.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/WriteBatchTest.kt index b06ac071c..f5e8ece3f 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/WriteBatchTest.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/WriteBatchTest.kt @@ -12,11 +12,7 @@ class WriteBatchTest : BaseFirebaseFirestoreTest() { .collection("testServerTestSetBatch") @Test - fun testSetBatch() = runTest { - val doc1 = collection - .document("test1") - val doc2 = collection - .document("test2") + fun testSetBatch() = testBatch { doc1, doc2 -> val batch = firestore.batch() batch.set( documentRef = doc1, @@ -41,11 +37,7 @@ class WriteBatchTest : BaseFirebaseFirestoreTest() { } @Test - fun testSetBatchDoesNotEncodeEmptyValues() = runTest { - val doc1 = collection - .document("test1") - val doc2 = collection - .document("test2") + fun testSetBatchDoesNotEncodeEmptyValues() = testBatch { doc1, doc2 -> val batch = firestore.batch() batch.set( documentRef = doc1, @@ -72,25 +64,19 @@ class WriteBatchTest : BaseFirebaseFirestoreTest() { } @Test - fun testUpdateBatch() = runTest { - val doc1 = collection - .document("test1").apply { - set( - FirestoreTest( - prop1 = "prop1", - time = 123.0, - ), - ) - } - val doc2 = collection - .document("test2").apply { - set( - FirestoreTest( - prop1 = "prop2", - time = 456.0, - ), - ) - } + fun testUpdateBatch() = testBatch { doc1, doc2 -> + doc1.set( + FirestoreTest( + prop1 = "prop1", + time = 123.0, + ), + ) + doc2.set( + FirestoreTest( + prop1 = "prop2", + time = 456.0, + ), + ) val batch = firestore.batch() batch.update( @@ -120,25 +106,19 @@ class WriteBatchTest : BaseFirebaseFirestoreTest() { } @Test - fun testUpdateBatchDoesNotEncodeEmptyValues() = runTest { - val doc1 = collection - .document("test1").apply { - set( - FirestoreTest( - prop1 = "prop1", - time = 123.0, - ), - ) - } - val doc2 = collection - .document("test2").apply { - set( - FirestoreTest( - prop1 = "prop2", - time = 456.0, - ), - ) - } + fun testUpdateBatchDoesNotEncodeEmptyValues() = testBatch { doc1, doc2 -> + doc1.set( + FirestoreTest( + prop1 = "prop1", + time = 123.0, + ), + ) + doc2.set( + FirestoreTest( + prop1 = "prop2", + time = 456.0, + ), + ) val batch = firestore.batch() batch.update( documentRef = doc1, @@ -169,38 +149,34 @@ class WriteBatchTest : BaseFirebaseFirestoreTest() { } @Test - fun testUpdateFieldValuesBatch() = runTest { - val doc1 = collection.document("test1").apply { - set( - FirestoreTest( - prop1 = "prop1", - time = 123.0, - duration = 800.milliseconds, - ), - ) - } + fun testUpdateFieldValuesBatch() = testBatch { doc1, doc2 -> + doc1.set( + FirestoreTest( + prop1 = "prop1", + time = 123.0, + duration = 800.milliseconds, + ), + ) - val doc2 = collection.document("test2").apply { - set( - FirestoreTest( - prop1 = "prop2", - time = 456.0, - duration = 700.milliseconds, - ), - ) - } + doc2.set( + FirestoreTest( + prop1 = "prop2", + time = 456.0, + duration = 700.milliseconds, + ), + ) val batch = firestore.batch() batch.update(doc1) { FirestoreTest::prop1.name to "prop1-updated" FieldPath(FirestoreTest::optional.name) to "notNull" - FirestoreTest::duration.name.to(DurationAsLongSerializer(), 300.milliseconds) + FirestoreTest::duration.name.to(DurationAsIntSerializer(), 300.milliseconds) FieldPath(FirestoreTest::nested.name).to(NestedObject.serializer(), NestedObject("nested")) } batch.update(doc2) { FirestoreTest::prop1.name to "prop2-updated" FieldPath(FirestoreTest::optional.name) to "alsoNotNull" - FirestoreTest::duration.name.to(DurationAsLongSerializer(), 200.milliseconds) + FirestoreTest::duration.name.to(DurationAsIntSerializer(), 200.milliseconds) FieldPath(FirestoreTest::nested.name).to(NestedObject.serializer(), NestedObject("alsoNested")) } batch.commit() @@ -217,4 +193,18 @@ class WriteBatchTest : BaseFirebaseFirestoreTest() { assertEquals(200.milliseconds, updatedDoc2.duration) assertEquals(NestedObject("alsoNested"), updatedDoc2.nested) } + + private fun testBatch(block: suspend (DocumentReference, DocumentReference) -> Unit) = runTest { + val doc1 = collection + .document("test1") + val doc2 = collection + .document("test2") + + try { + block(doc1, doc2) + } finally { + doc1.delete() + doc2.delete() + } + } } diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index d55d5dce9..7f79a7ebc 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -45,7 +45,7 @@ abstract class BaseFirebaseFirestoreTest { val optional: String? = null, val nested: NestedObject? = null, val nestedList: List = emptyList(), - @Serializable(with = DurationAsLongSerializer::class) + @Serializable(with = DurationAsIntSerializer::class) val duration: Duration = Duration.ZERO, ) @@ -54,15 +54,16 @@ abstract class BaseFirebaseFirestoreTest { val prop2: String, ) - class DurationAsLongSerializer : KSerializer { + // Long would be better but JS does not seem to support it on the Firebase level https://stackoverflow.com/questions/31930406/storing-long-type-in-firebase + class DurationAsIntSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("millisecondsSinceEpoch", PrimitiveKind.LONG) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("millisecondsSinceEpoch", PrimitiveKind.INT) override fun serialize(encoder: Encoder, value: Duration) { - encoder.encodeLong(value.inWholeMilliseconds) + encoder.encodeInt(value.inWholeMilliseconds.toInt()) } - override fun deserialize(decoder: Decoder): Duration = decoder.decodeLong().milliseconds + override fun deserialize(decoder: Decoder): Duration = decoder.decodeInt().milliseconds } lateinit var firebaseApp: FirebaseApp diff --git a/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/ContextSwitchTest.kt b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/ContextSwitchTest.kt index e7c6fc04f..494225234 100644 --- a/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/ContextSwitchTest.kt +++ b/firebase-firestore/src/iosTest/kotlin/dev/gitlive/firebase/firestore/ContextSwitchTest.kt @@ -121,28 +121,38 @@ class ContextSwitchTest { }, ) { data -> - fun getDocument() = firestore.collection("fieldValuesOps") + val doc = firestore.collection("fieldValuesOps") .document("fieldValuesOps") - // store - getDocument().set(strategy = TestData.serializer(), data = TestData(data.initial), merge = false) - - // append & verify - getDocument().update(data.updates[0].op) - - var savedData = getDocument().get().data(TestData.serializer()) - assertEquals(data.updates[0].expected, savedData.values) - - // remove & verify - getDocument().update(data.updates[1].op) - savedData = getDocument().get().data(TestData.serializer()) - assertEquals(data.updates[1].expected, savedData.values) + try { + // store + doc.set( + strategy = TestData.serializer(), + data = TestData(data.initial), + merge = false, + ) - val list = getDocument().get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) - assertEquals(data.updates[1].expected, list) - // delete & verify - getDocument().update(data.updates[2].op) - val deletedList = getDocument().get().get(TestData::values.name, ListSerializer(Int.serializer()).nullable) - assertEquals(data.updates[2].expected, deletedList) + // append & verify + doc.update(data.updates[0].op) + + var savedData = doc.get().data(TestData.serializer()) + assertEquals(data.updates[0].expected, savedData.values) + + // remove & verify + doc.update(data.updates[1].op) + savedData = doc.get().data(TestData.serializer()) + assertEquals(data.updates[1].expected, savedData.values) + + val list = doc.get() + .get(TestData::values.name, ListSerializer(Int.serializer()).nullable) + assertEquals(data.updates[1].expected, list) + // delete & verify + doc.update(data.updates[2].op) + val deletedList = doc.get() + .get(TestData::values.name, ListSerializer(Int.serializer()).nullable) + assertEquals(data.updates[2].expected, deletedList) + } finally { + doc.delete() + } } }