Skip to content

Commit 45b69eb

Browse files
authored
Merge pull request #38 from fmasa/auth-emulator
[WIP] Auth: Add support for emulator
2 parents 94f0d1d + 9671836 commit 45b69eb

File tree

10 files changed

+164
-39
lines changed

10 files changed

+164
-39
lines changed

.firebaserc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

.github/workflows/build-pr.yml

+7-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ jobs:
1111
with:
1212
distribution: 'zulu'
1313
java-version: 17
14-
- name: Build
15-
uses: eskatos/gradle-command-action@v3
14+
- name: Set up Node.js 20
15+
uses: actions/setup-node@v4
1616
with:
17-
arguments: build
17+
node-version: 20
18+
- name: Install Firebase CLI
19+
run: npm install -g firebase-tools
20+
- name: Build
21+
run: firebase emulators:exec --project my-firebase-project --import=src/test/resources/firebase_data './gradlew build'

firebase.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"emulators": {
3+
"auth": {
4+
"port": 9099
5+
},
6+
"ui": {
7+
"enabled": true
8+
},
9+
"singleProjectMode": true
10+
}
11+
}

src/main/java/com/google/firebase/auth/FirebaseAuth.kt

+26-9
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ import java.util.concurrent.TimeUnit
4343

4444
val jsonParser = Json { ignoreUnknownKeys = true }
4545

46+
class UrlFactory(
47+
private val app: FirebaseApp,
48+
private val emulatorUrl: String? = null
49+
) {
50+
fun buildUrl(uri: String): String {
51+
return "${emulatorUrl ?: "https://"}$uri?key=${app.options.apiKey}"
52+
}
53+
}
54+
4655
@Serializable
4756
class FirebaseUserImpl private constructor(
4857
@Transient
@@ -52,17 +61,20 @@ class FirebaseUserImpl private constructor(
5261
val idToken: String,
5362
val refreshToken: String,
5463
val expiresIn: Int,
55-
val createdAt: Long
64+
val createdAt: Long,
65+
@Transient
66+
private val urlFactory: UrlFactory = UrlFactory(app)
5667
) : FirebaseUser() {
5768

58-
constructor(app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false) : this(
69+
constructor(app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, urlFactory: UrlFactory = UrlFactory(app)) : this(
5970
app,
6071
isAnonymous,
6172
data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "",
6273
data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content,
6374
data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content,
6475
data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int,
65-
data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis()
76+
data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis(),
77+
urlFactory
6678
)
6779

6880
val claims: Map<String, Any?> by lazy {
@@ -85,7 +97,7 @@ class FirebaseUserImpl private constructor(
8597
val source = TaskCompletionSource<Void>()
8698
val body = RequestBody.create(FirebaseAuth.getInstance(app).json, JsonObject(mapOf("idToken" to JsonPrimitive(idToken))).toString())
8799
val request = Request.Builder()
88-
.url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount?key=" + app.options.apiKey)
100+
.url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount"))
89101
.post(body)
90102
.build()
91103
FirebaseAuth.getInstance(app).client.newCall(request).enqueue(object : Callback {
@@ -184,11 +196,13 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
184196
}
185197
}
186198

199+
private var urlFactory = UrlFactory(app)
200+
187201
fun signInAnonymously(): Task<AuthResult> {
188202
val source = TaskCompletionSource<AuthResult>()
189203
val body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString())
190204
val request = Request.Builder()
191-
.url("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=" + app.options.apiKey)
205+
.url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:signUp"))
192206
.post(body)
193207
.build()
194208
client.newCall(request).enqueue(object : Callback {
@@ -220,7 +234,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
220234
JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString()
221235
)
222236
val request = Request.Builder()
223-
.url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=" + app.options.apiKey)
237+
.url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"))
224238
.post(body)
225239
.build()
226240
client.newCall(request).enqueue(object : Callback {
@@ -252,7 +266,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
252266
JsonObject(mapOf("email" to JsonPrimitive(email), "password" to JsonPrimitive(password), "returnSecureToken" to JsonPrimitive(true))).toString()
253267
)
254268
val request = Request.Builder()
255-
.url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=" + app.options.apiKey)
269+
.url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword"))
256270
.post(body)
257271
.build()
258272
client.newCall(request).enqueue(object : Callback {
@@ -336,7 +350,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
336350
).toString()
337351
)
338352
val request = Request.Builder()
339-
.url("https://securetoken.googleapis.com/v1/token?key=" + app.options.apiKey)
353+
.url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token"))
340354
.post(body)
341355
.build()
342356

@@ -439,5 +453,8 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
439453
fun signInWithEmailLink(email: String, link: String): Task<AuthResult> = TODO()
440454

441455
fun setLanguageCode(value: String): Nothing = TODO()
442-
fun useEmulator(host: String, port: Int): Unit = TODO()
456+
457+
fun useEmulator(host: String, port: Int) {
458+
urlFactory = UrlFactory(app, "http://$host:$port/")
459+
}
443460
}

src/test/kotlin/AuthTest.kt

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import com.google.firebase.auth.FirebaseAuth
2+
import com.google.firebase.auth.FirebaseAuthInvalidUserException
3+
import kotlinx.coroutines.runBlocking
4+
import kotlinx.coroutines.tasks.await
5+
import kotlinx.coroutines.test.runTest
6+
import org.junit.Assert.assertEquals
7+
import org.junit.Assert.assertThrows
8+
import org.junit.Test
9+
10+
class AuthTest : FirebaseTest() {
11+
private fun createAuth(): FirebaseAuth {
12+
return FirebaseAuth(app).apply {
13+
useEmulator("localhost", 9099)
14+
}
15+
}
16+
17+
@Test
18+
fun `should authenticate via anonymous auth`() = runTest {
19+
val auth = createAuth()
20+
21+
auth.signInAnonymously().await()
22+
23+
assertEquals(true, auth.currentUser?.isAnonymous)
24+
}
25+
26+
@Test
27+
fun `should authenticate via email and password`() = runTest {
28+
val auth = createAuth()
29+
30+
auth.signInWithEmailAndPassword("[email protected]", "securepassword").await()
31+
32+
assertEquals(false, auth.currentUser?.isAnonymous)
33+
}
34+
35+
@Test
36+
fun `should throw exception on invalid password`() {
37+
val auth = createAuth()
38+
39+
val exception = assertThrows(FirebaseAuthInvalidUserException::class.java) {
40+
runBlocking {
41+
auth.signInWithEmailAndPassword("[email protected]", "wrongpassword").await()
42+
}
43+
}
44+
45+
assertEquals("INVALID_PASSWORD", exception.errorCode)
46+
}
47+
}

src/test/kotlin/FirebaseTest.kt

+24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
1+
import android.app.Application
2+
import com.google.firebase.Firebase
13
import com.google.firebase.FirebaseApp
4+
import com.google.firebase.FirebaseOptions
5+
import com.google.firebase.FirebasePlatform
6+
import com.google.firebase.initialize
27
import org.junit.Before
8+
import java.io.File
39

410
abstract class FirebaseTest {
11+
protected val app: FirebaseApp get() {
12+
val options = FirebaseOptions.Builder()
13+
.setProjectId("my-firebase-project")
14+
.setApplicationId("1:27992087142:android:ce3b6448250083d1")
15+
.setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw")
16+
.build()
17+
18+
return Firebase.initialize(Application(), options)
19+
}
20+
521
@Before
622
fun beforeEach() {
23+
FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() {
24+
val storage = mutableMapOf<String, String>()
25+
override fun store(key: String, value: String) = storage.set(key, value)
26+
override fun retrieve(key: String) = storage[key]
27+
override fun clear(key: String) { storage.remove(key) }
28+
override fun log(msg: String) = println(msg)
29+
override fun getDatabasePath(name: String) = File("./build/$name")
30+
})
731
FirebaseApp.clearInstancesForTest()
832
}
933
}

src/test/kotlin/FirestoreTest.kt

+4-27
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,19 @@
1-
import android.app.Application
21
import com.google.firebase.Firebase
3-
import com.google.firebase.FirebaseOptions
4-
import com.google.firebase.FirebasePlatform
52
import com.google.firebase.firestore.firestore
6-
import com.google.firebase.initialize
73
import kotlinx.coroutines.tasks.await
84
import kotlinx.coroutines.test.runTest
95
import org.junit.Assert.assertEquals
10-
import org.junit.Before
116
import org.junit.Test
12-
import java.io.File
137

148
class FirestoreTest : FirebaseTest() {
15-
@Before
16-
fun initialize() {
17-
FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() {
18-
val storage = mutableMapOf<String, String>()
19-
override fun store(key: String, value: String) = storage.set(key, value)
20-
override fun retrieve(key: String) = storage[key]
21-
override fun clear(key: String) { storage.remove(key) }
22-
override fun log(msg: String) = println(msg)
23-
override fun getDatabasePath(name: String) = File("./build/$name")
24-
})
25-
val options = FirebaseOptions.Builder()
26-
.setProjectId("my-firebase-project")
27-
.setApplicationId("1:27992087142:android:ce3b6448250083d1")
28-
.setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw")
29-
// setDatabaseURL(...)
30-
// setStorageBucket(...)
31-
.build()
32-
Firebase.initialize(Application(), options)
33-
Firebase.firestore.disableNetwork()
34-
}
359

3610
@Test
3711
fun testFirestore(): Unit = runTest {
12+
val firestore = Firebase.firestore(app)
13+
firestore.disableNetwork().await()
14+
3815
val data = Data("jim")
39-
val doc = Firebase.firestore.document("sally/jim")
16+
val doc = firestore.document("sally/jim")
4017
doc.set(data)
4118
assertEquals(data, doc.get().await().toObject(Data::class.java))
4219
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"kind": "identitytoolkit#DownloadAccountResponse",
3+
"users": [
4+
{
5+
"localId": "Ijat10t0F1gvH1VrClkkSqEcId1p",
6+
"lastLoginAt": "1728509249920",
7+
"displayName": "",
8+
"photoUrl": "",
9+
"emailVerified": true,
10+
"email": "[email protected]",
11+
"salt": "fakeSaltHsRxYqy9iKVQRLwz8975",
12+
"passwordHash": "fakeHash:salt=fakeSaltHsRxYqy9iKVQRLwz8975:password=securepassword",
13+
"passwordUpdatedAt": 1728509249921,
14+
"validSince": "1728509249",
15+
"mfaInfo": [],
16+
"createdAt": "1728509249920",
17+
"providerUserInfo": [
18+
{
19+
"providerId": "password",
20+
"email": "[email protected]",
21+
"federatedId": "[email protected]",
22+
"rawId": "[email protected]",
23+
"displayName": "",
24+
"photoUrl": ""
25+
}
26+
]
27+
}
28+
]
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"signIn": {
3+
"allowDuplicateEmails": false
4+
},
5+
"emailPrivacyConfig": {
6+
"enableImprovedEmailPrivacy": false
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"version": "13.3.1",
3+
"auth": {
4+
"version": "13.3.1",
5+
"path": "auth_export"
6+
}
7+
}

0 commit comments

Comments
 (0)