Skip to content

Commit 3dea253

Browse files
fmasaJacobCube
authored andcommitted
Test Auth against emulator
1 parent 694da88 commit 3dea253

File tree

10 files changed

+169
-41
lines changed

10 files changed

+169
-41
lines changed

.firebaserc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

.github/workflows/build-pr.yml

Lines changed: 7 additions & 3 deletions
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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ import java.util.concurrent.TimeUnit
4343

4444
internal 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 internal constructor(
4857
@Transient
@@ -53,14 +62,17 @@ class FirebaseUserImpl internal constructor(
5362
val refreshToken: String,
5463
val expiresIn: Int,
5564
val createdAt: Long,
56-
override val email: String?
65+
override val email: String?,
66+
@Transient
67+
private val urlFactory: UrlFactory = UrlFactory(app)
5768
) : FirebaseUser() {
5869

5970
constructor(
6071
app: FirebaseApp,
6172
data: JsonObject,
6273
isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false,
63-
email: String? = data.getOrElse("email") { null }?.jsonPrimitive?.contentOrNull
74+
email: String? = data.getOrElse("email") { null }?.jsonPrimitive?.contentOrNull,
75+
urlFactory: UrlFactory = UrlFactory(app)
6476
) : this(
6577
app = app,
6678
isAnonymous = isAnonymous,
@@ -69,7 +81,8 @@ class FirebaseUserImpl internal constructor(
6981
refreshToken = data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content,
7082
expiresIn = data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int,
7183
createdAt = data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis(),
72-
email = email
84+
email = email,
85+
urlFactory = urlFactory
7386
)
7487

7588
val claims: Map<String, Any?> by lazy {
@@ -92,7 +105,7 @@ class FirebaseUserImpl internal constructor(
92105
val source = TaskCompletionSource<Void>()
93106
val body = RequestBody.create(FirebaseAuth.getInstance(app).json, JsonObject(mapOf("idToken" to JsonPrimitive(idToken))).toString())
94107
val request = Request.Builder()
95-
.url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount?key=" + app.options.apiKey)
108+
.url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount"))
96109
.post(body)
97110
.build()
98111
FirebaseAuth.getInstance(app).client.newCall(request).enqueue(object : Callback {
@@ -194,7 +207,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
194207
): TaskCompletionSource<AuthResult> {
195208
val source = TaskCompletionSource<AuthResult>()
196209
val request = Request.Builder()
197-
.url("$url?key=" + app.options.apiKey)
210+
.url(urlFactory.buildUrl(url))
198211
.post(body)
199212
.build()
200213

@@ -279,9 +292,11 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
279292
}
280293
}
281294

295+
private var urlFactory = UrlFactory(app)
296+
282297
fun signInAnonymously(): Task<AuthResult> {
283298
val source = enqueueAuthPost(
284-
url = "https://identitytoolkit.googleapis.com/v1/accounts:signUp",
299+
url = "identitytoolkit.googleapis.com/v1/accounts:signUp",
285300
body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString()),
286301
setResult = { responseBody ->
287302
FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject, isAnonymous = true)
@@ -292,7 +307,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
292307

293308
fun signInWithCustomToken(customToken: String): Task<AuthResult> {
294309
val source = enqueueAuthPost(
295-
url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken",
310+
url = "www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken",
296311
body = RequestBody.create(
297312
json,
298313
JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString()
@@ -306,7 +321,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
306321

307322
fun createUserWithEmailAndPassword(email: String, password: String): Task<AuthResult> {
308323
val source = enqueueAuthPost(
309-
url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser",
324+
url = "www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser",
310325
body = RequestBody.create(
311326
json,
312327
JsonObject(
@@ -324,9 +339,10 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
324339
return source.task
325340
}
326341

342+
327343
fun signInWithEmailAndPassword(email: String, password: String): Task<AuthResult> {
328344
val source = enqueueAuthPost(
329-
url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword",
345+
url = "www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword",
330346
body = RequestBody.create(
331347
json,
332348
JsonObject(
@@ -406,7 +422,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
406422
).toString()
407423
)
408424
val request = Request.Builder()
409-
.url("https://securetoken.googleapis.com/v1/token?key=" + app.options.apiKey)
425+
.url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token"))
410426
.post(body)
411427
.tag(REFRESH_TOKEN_TAG)
412428
.build()
@@ -555,7 +571,12 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
555571
idTokenListeners.remove(listener)
556572
}
557573

574+
fun useEmulator(host: String, port: Int) {
575+
urlFactory = UrlFactory(app, "http://$host:$port/")
576+
}
577+
558578
fun sendPasswordResetEmail(email: String, settings: ActionCodeSettings?): Task<Unit> = TODO()
579+
fun signInWithCredential(authCredential: AuthCredential): Task<AuthResult> = TODO()
559580
fun checkActionCode(code: String): Task<ActionCodeResult> = TODO()
560581
fun confirmPasswordReset(code: String, newPassword: String): Task<Unit> = TODO()
561582
fun fetchSignInMethodsForEmail(email: String): Task<SignInMethodQueryResult> = TODO()
@@ -568,5 +589,4 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
568589
fun signInWithEmailLink(email: String, link: String): Task<AuthResult> = TODO()
569590

570591
fun setLanguageCode(value: String): Nothing = TODO()
571-
fun useEmulator(host: String, port: Int): Unit = TODO()
572592
}

src/test/kotlin/AuthTest.kt

Lines changed: 47 additions & 0 deletions
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

Lines changed: 24 additions & 0 deletions
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

Lines changed: 4 additions & 27 deletions
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
}
Lines changed: 29 additions & 0 deletions
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+
}
Lines changed: 8 additions & 0 deletions
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+
}
Lines changed: 7 additions & 0 deletions
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)