Skip to content

Commit 2a673e4

Browse files
authored
Add useEmulator() API (#123)
* Add using emulator for firebase-auth * Try test actions with emulator * Fix emulator project ID * Update .gitignore * Implement Firestore useEmulator() API * Implement Realtime Database useEmulator() API * Deprecate useFunctionsEmulator() * Revert GitHub Actions config * Remove unnecessary XML config * Cleanup * Move test firebase configs to 'test' dir * Use Android's clear text traffic config for test target only * Fix "TypeError: e.log is not a function" error * Map "auth/email-already-in-use" JS error code to FirebaseAuthUserCollisionException
1 parent 061ef51 commit 2a673e4

File tree

34 files changed

+165
-31
lines changed

34 files changed

+165
-31
lines changed

.github/workflows/pull_request.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ jobs:
2222
run: chmod +x gradlew
2323
- name: Install Carthage
2424
run: brew list carthage || brew install carthage
25+
- name: Install Firebase tools
26+
run: npm install -g firebase-tools
27+
- name: Start Firebase emulator
28+
run: "firebase emulators:start --config=./test/firebase.json &"
2529
- name: Assemble
2630
run: ./gradlew assemble
2731
- name: Run JS Tests

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ firebase-database/src/iosMain/c_interop/modules/
1313
Firebase*.zip
1414
/Firebase
1515
/.DS_Store
16+
*.log
1617

1718

1819
/**/Cartfile.resolved

firebase-auth/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ android {
3131
getByName("main") {
3232
manifest.srcFile("src/androidMain/AndroidManifest.xml")
3333
}
34-
getByName("androidTest").java.srcDir(file("src/androidAndroidTest/kotlin"))
34+
getByName("androidTest"){
35+
java.srcDir(file("src/androidAndroidTest/kotlin"))
36+
manifest.srcFile("src/androidAndroidTest/AndroidManifest.xml")
37+
}
3538
}
3639
testOptions {
3740
unitTests.apply {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="dev.gitlive.firebase.auth">
3+
4+
<application android:usesCleartextTraffic="true" />
5+
</manifest>

firebase-auth/src/androidAndroidTest/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package dev.gitlive.firebase.auth
88
import androidx.test.platform.app.InstrumentationRegistry
99
import kotlinx.coroutines.runBlocking
1010

11+
actual val emulatorHost: String = "10.0.2.2"
12+
1113
actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext
1214

13-
actual fun runTest(test: suspend () -> Unit) = runBlocking { test() }
15+
actual fun runTest(test: suspend () -> Unit) = runBlocking { test() }

firebase-auth/src/androidMain/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ actual class FirebaseAuth internal constructor(val android: com.google.firebase.
5151
get() = android.languageCode ?: ""
5252
set(value) { android.setLanguageCode(value) }
5353

54+
5455
actual suspend fun applyActionCode(code: String) = android.applyActionCode(code).await().run { Unit }
5556
actual suspend fun confirmPasswordReset(code: String, newPassword: String) = android.confirmPasswordReset(code, newPassword).await().run { Unit }
5657

@@ -102,6 +103,7 @@ actual class FirebaseAuth internal constructor(val android: com.google.firebase.
102103
} as T
103104
}
104105

106+
actual fun useEmulator(host: String, port: Int) = android.useEmulator(host, port)
105107
}
106108

107109
actual class AuthResult internal constructor(val android: com.google.firebase.auth.AuthResult) {

firebase-auth/src/commonMain/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ expect class FirebaseAuth {
3434
suspend fun signOut()
3535
suspend fun updateCurrentUser(user: FirebaseUser)
3636
suspend fun verifyPasswordResetCode(code: String): String
37+
fun useEmulator(host: String, port: Int)
3738
}
3839

3940
expect class AuthResult {

firebase-auth/src/commonTest/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import dev.gitlive.firebase.*
88
import kotlin.random.Random
99
import kotlin.test.*
1010

11+
expect val emulatorHost: String
1112
expect val context: Any
1213
expect fun runTest(test: suspend () -> Unit)
1314

@@ -17,22 +18,26 @@ class FirebaseAuthTest {
1718
fun initializeFirebase() {
1819
Firebase
1920
.takeIf { Firebase.apps(context).isEmpty() }
20-
?.initialize(
21-
context,
22-
FirebaseOptions(
23-
applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
24-
apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
25-
databaseUrl = "https://fir-kotlin-sdk.firebaseio.com",
26-
storageBucket = "fir-kotlin-sdk.appspot.com",
27-
projectId = "fir-kotlin-sdk"
21+
?.apply {
22+
initialize(
23+
context,
24+
FirebaseOptions(
25+
applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
26+
apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
27+
databaseUrl = "https://fir-kotlin-sdk.firebaseio.com",
28+
storageBucket = "fir-kotlin-sdk.appspot.com",
29+
projectId = "fir-kotlin-sdk"
30+
)
2831
)
29-
)
32+
Firebase.auth.useEmulator(emulatorHost, 9099)
33+
}
3034
}
3135

3236
@Test
3337
fun testSignInWithUsernameAndPassword() = runTest {
38+
val uid = getTestUid("test@test.com", "test123")
3439
val result = Firebase.auth.signInWithEmailAndPassword("test@test.com", "test123")
35-
assertEquals("mn8kgIFnxLO7il8GpTa5g0ObP6I2", result.user!!.uid)
40+
assertEquals(uid, result.user!!.uid)
3641
}
3742

3843
@Test
@@ -85,9 +90,27 @@ class FirebaseAuthTest {
8590

8691
@Test
8792
fun testSignInWithCredential() = runTest {
93+
val uid = getTestUid("test@test.com", "test123")
8894
val credential = EmailAuthProvider.credential("test@test.com", "test123")
8995
val result = Firebase.auth.signInWithCredential(credential)
90-
assertEquals("mn8kgIFnxLO7il8GpTa5g0ObP6I2", result.user!!.uid)
96+
assertEquals(uid, result.user!!.uid)
97+
}
9198

99+
private suspend fun getTestUid(email: String, password: String): String {
100+
val uid = Firebase.auth.let {
101+
val user = try {
102+
it.createUserWithEmailAndPassword(email, password).user
103+
} catch (e: FirebaseAuthUserCollisionException) {
104+
// the user already exists, just sign in for getting user's ID
105+
it.signInWithEmailAndPassword(email, password).user
106+
}
107+
user!!.uid
108+
}
109+
110+
check(Firebase.auth.currentUser != null)
111+
Firebase.auth.signOut()
112+
check(Firebase.auth.currentUser == null)
113+
114+
return uid
92115
}
93-
}
116+
}

firebase-auth/src/iosMain/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ actual class FirebaseAuth internal constructor(val ios: FIRAuth) {
8989
else -> throw UnsupportedOperationException(result.operation.toString())
9090
} as T
9191
}
92+
93+
actual fun useEmulator(host: String, port: Int) = ios.useEmulatorWithHost(host, port.toLong())
9294
}
9395

9496
actual class AuthResult internal constructor(val ios: FIRAuthDataResult) {

firebase-auth/src/iosTest/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ package dev.gitlive.firebase.auth
77
import kotlinx.coroutines.*
88
import platform.Foundation.*
99

10+
actual val emulatorHost: String = "localhost"
11+
1012
actual val context: Any = Unit
1113

1214
actual fun runTest(test: suspend () -> Unit) = runBlocking {
@@ -19,4 +21,4 @@ actual fun runTest(test: suspend () -> Unit) = runBlocking {
1921
yield()
2022
}
2123
testRun.await()
22-
}
24+
}

firebase-auth/src/jsMain/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ actual class FirebaseAuth internal constructor(val js: firebase.auth.Auth) {
9292
else -> throw UnsupportedOperationException(result.operation)
9393
} as T
9494
}
95+
96+
actual fun useEmulator(host: String, port: Int) = rethrow { js.useEmulator("http://$host:$port") }
9597
}
9698

9799
actual class AuthResult internal constructor(val js: firebase.auth.AuthResult) {
@@ -144,6 +146,7 @@ private fun errorToException(cause: dynamic) = when(val code = cause.code?.toStr
144146
"auth/maximum-second-factor-count-exceeded",
145147
"auth/second-factor-already-in-use" -> FirebaseAuthMultiFactorException(code, cause)
146148
"auth/credential-already-in-use" -> FirebaseAuthUserCollisionException(code, cause)
149+
"auth/email-already-in-use" -> FirebaseAuthUserCollisionException(code, cause)
147150
"auth/invalid-email" -> FirebaseAuthEmailException(code, cause)
148151

149152
// "auth/app-deleted" ->

firebase-auth/src/jsTest/kotlin/dev/gitlive/firebase/auth/auth.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ package dev.gitlive.firebase.auth
77
import kotlinx.coroutines.GlobalScope
88
import kotlinx.coroutines.promise
99

10+
actual val emulatorHost: String = "localhost"
11+
1012
actual val context: Any = Unit
1113

1214
actual fun runTest(test: suspend () -> Unit) = GlobalScope
1315
.promise {
1416
try {
1517
test()
1618
} catch (e: dynamic) {
17-
e.log()
19+
(e as? Throwable)?.log()
1820
throw e
1921
}
2022
}.asDynamic()
@@ -25,4 +27,4 @@ internal fun Throwable.log() {
2527
console.error("Caused by:")
2628
it.log()
2729
}
28-
}
30+
}

firebase-common/src/jsMain/kotlin/dev/gitlive/firebase/externals.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ external object firebase {
4646
val currentUser: user.User?
4747
var languageCode: String?
4848

49+
fun useEmulator(url: String)
4950
fun applyActionCode(code: String): Promise<Unit>
5051
fun checkActionCode(code: String): Promise<ActionCodeInfo>
5152
fun confirmPasswordReset(code: String, newPassword: String): Promise<Unit>
@@ -258,6 +259,7 @@ external object firebase {
258259
class Functions {
259260
fun httpsCallable(name: String, options: Json?): HttpsCallable
260261
fun useFunctionsEmulator(origin: String)
262+
fun useEmulator(host: String, port: Int)
261263
}
262264
interface HttpsCallableResult {
263265
val data: Any?
@@ -274,6 +276,7 @@ external object firebase {
274276

275277
open class Database {
276278
fun ref(path: String? = definedExternally): Reference
279+
fun useEmulator(host: String, port: Int)
277280
}
278281
open class ThenableReference : Reference
279282

@@ -340,6 +343,7 @@ external object firebase {
340343
fun settings(settings: Json)
341344
fun enablePersistence(): Promise<Unit>
342345
fun clearPersistence(): Promise<Unit>
346+
fun useEmulator(host: String, port: Int)
343347
}
344348

345349
open class Timestamp {

firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ actual class FirebaseDatabase internal constructor(val android: com.google.fireb
7373

7474
actual fun setLoggingEnabled(enabled: Boolean) =
7575
android.setLogLevel(Logger.Level.DEBUG.takeIf { enabled } ?: Logger.Level.NONE)
76+
77+
actual fun useEmulator(host: String, port: Int) =
78+
android.useEmulator(host, port)
7679
}
7780

7881
actual open class Query internal constructor(

firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ expect class FirebaseDatabase {
2727
fun reference(path: String): DatabaseReference
2828
fun setPersistenceEnabled(enabled: Boolean)
2929
fun setLoggingEnabled(enabled: Boolean)
30+
fun useEmulator(host: String, port: Int)
3031
}
3132

3233
data class ChildEvent internal constructor(

firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) {
5757

5858
actual fun setLoggingEnabled(enabled: Boolean) =
5959
FIRDatabase.setLoggingEnabled(enabled)
60+
61+
actual fun useEmulator(host: String, port: Int) =
62+
ios.useEmulatorWithHost(host, port.toLong())
6063
}
6164

6265
fun Type.toEventType() = when(this) {
@@ -237,4 +240,4 @@ suspend fun <T> CompletableDeferred<T>.awaitWhileOnline(): T = coroutineScope {
237240
onAwait { it.also { notConnected.cancel() } }
238241
notConnected.onReceive { throw DatabaseException("Database not connected") }
239242
}
240-
}
243+
}

firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ actual class FirebaseDatabase internal constructor(val js: firebase.database.Dat
3535
actual fun reference(path: String) = rethrow { DatabaseReference(js.ref(path)) }
3636
actual fun setPersistenceEnabled(enabled: Boolean) {}
3737
actual fun setLoggingEnabled(enabled: Boolean) = rethrow { firebase.database.enableLogging(enabled) }
38+
actual fun useEmulator(host: String, port: Int) = rethrow { js.useEmulator(host, port) }
3839
}
3940

4041
actual open class Query internal constructor(open val js: firebase.database.Query) {

firebase-firestore/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ android {
1818
getByName("main") {
1919
manifest.srcFile("src/androidMain/AndroidManifest.xml")
2020
}
21-
getByName("androidTest").java.srcDir(file("src/androidAndroidTest/kotlin"))
21+
getByName("androidTest"){
22+
java.srcDir(file("src/androidAndroidTest/kotlin"))
23+
manifest.srcFile("src/androidAndroidTest/AndroidManifest.xml")
24+
}
2225
}
2326
testOptions {
2427
unitTests.apply {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="dev.gitlive.firebase.auth">
3+
4+
<application android:usesCleartextTraffic="true" />
5+
</manifest>

firebase-firestore/src/androidAndroidTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package dev.gitlive.firebase.firestore
88
import androidx.test.platform.app.InstrumentationRegistry
99
import kotlinx.coroutines.runBlocking
1010

11+
actual val emulatorHost: String = "10.0.2.2"
12+
1113
actual val context: Any = InstrumentationRegistry.getInstrumentation().targetContext
1214

1315
actual fun runTest(test: suspend () -> Unit) = runBlocking { test() }

firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package dev.gitlive.firebase.firestore
77

88
import com.google.firebase.Timestamp
99
import com.google.firebase.firestore.FieldValue
10+
import com.google.firebase.firestore.FirebaseFirestoreSettings
1011
import com.google.firebase.firestore.SetOptions
1112
import dev.gitlive.firebase.*
1213
import dev.gitlive.firebase.firestore.encode
@@ -61,6 +62,13 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba
6162
actual suspend fun clearPersistence() =
6263
android.clearPersistence().await()
6364
.run { Unit }
65+
66+
actual fun useEmulator(host: String, port: Int) {
67+
android.useEmulator(host, port)
68+
android.firestoreSettings = FirebaseFirestoreSettings.Builder()
69+
.setPersistenceEnabled(false)
70+
.build()
71+
}
6472
}
6573

6674
actual class WriteBatch(val android: com.google.firebase.firestore.WriteBatch) {

firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ expect class FirebaseFirestore {
2626
fun setLoggingEnabled(loggingEnabled: Boolean)
2727
suspend fun clearPersistence()
2828
suspend fun <T> runTransaction(func: suspend Transaction.() -> T): T
29+
fun useEmulator(host: String, port: Int)
2930
}
3031

3132
expect class Transaction {

firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import dev.gitlive.firebase.*
88
import kotlinx.serialization.*
99
import kotlin.test.*
1010

11+
expect val emulatorHost: String
1112
expect val context: Any
1213
expect fun runTest(test: suspend () -> Unit)
1314

@@ -20,16 +21,19 @@ class FirebaseFirestoreTest {
2021
fun initializeFirebase() {
2122
Firebase
2223
.takeIf { Firebase.apps(context).isEmpty() }
23-
?.initialize(
24-
context,
25-
FirebaseOptions(
26-
applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
27-
apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
28-
databaseUrl = "https://fir-kotlin-sdk.firebaseio.com",
29-
storageBucket = "fir-kotlin-sdk.appspot.com",
30-
projectId = "fir-kotlin-sdk"
24+
?.apply {
25+
initialize(
26+
context,
27+
FirebaseOptions(
28+
applicationId = "1:846484016111:ios:dd1f6688bad7af768c841a",
29+
apiKey = "AIzaSyCK87dcMFhzCz_kJVs2cT2AVlqOTLuyWV0",
30+
databaseUrl = "https://fir-kotlin-sdk.firebaseio.com",
31+
storageBucket = "fir-kotlin-sdk.appspot.com",
32+
projectId = "fir-kotlin-sdk"
33+
)
3134
)
32-
)
35+
Firebase.firestore.useEmulator(emulatorHost, 8080)
36+
}
3337
}
3438

3539
@Test

firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ actual class FirebaseFirestore(val ios: FIRFirestore) {
5353

5454
actual suspend fun clearPersistence() =
5555
await { ios.clearPersistenceWithCompletion(it) }
56+
57+
actual fun useEmulator(host: String, port: Int) {
58+
ios.settings = ios.settings.apply {
59+
this.host = "$host:$port"
60+
persistenceEnabled = false
61+
sslEnabled = false
62+
}
63+
}
5664
}
5765

5866
@Suppress("UNCHECKED_CAST")

0 commit comments

Comments
 (0)