Skip to content

Commit aa0c563

Browse files
authored
Merge branch 'master' into signup_new_user
2 parents e51997d + 45b69eb commit aa0c563

File tree

10 files changed

+169
-43
lines changed

10 files changed

+169
-43
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 & 13 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,18 +81,19 @@ 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

75-
val claims: Map<String, Any?> by lazy {
88+
internal val claims: Map<String, Any?> by lazy {
7689
jsonParser
7790
.parseToJsonElement(String(Base64.getUrlDecoder().decode(idToken.split(".")[1])))
7891
.jsonObject
7992
.run { value as Map<String, Any?>? }
8093
.orEmpty()
8194
}
8295

83-
val JsonElement.value get(): Any? = when (this) {
96+
internal val JsonElement.value get(): Any? = when (this) {
8497
is JsonNull -> null
8598
is JsonArray -> map { it.value }
8699
is JsonObject -> jsonObject.mapValues { (_, it) -> it.value }
@@ -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 {
@@ -211,9 +224,9 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
211224
)
212225
} else {
213226
if(response.body()?.use { it.string() }?.also { responseBody ->
214-
user = setResult(responseBody)
215-
source.setResult(AuthResult { user })
216-
} == null) {
227+
user = setResult(responseBody)
228+
source.setResult(AuthResult { user })
229+
} == null) {
217230
source.setException(
218231
createAuthInvalidUserException("accounts", request, response)
219232
)
@@ -279,6 +292,8 @@ 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(
284299
url = "https://identitytoolkit.googleapis.com/v1/accounts:signUp",
@@ -358,8 +373,8 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
358373
?.contentOrNull
359374
?: "UNKNOWN_ERROR",
360375
"$action API returned an error, " +
361-
"with url [${request.method()}] ${request.url()} ${request.body()} -- " +
362-
"response [${response.code()}] ${response.message()} $body"
376+
"with url [${request.method()}] ${request.url()} ${request.body()} -- " +
377+
"response [${response.code()}] ${response.message()} $body"
363378
)
364379
}
365380

@@ -406,7 +421,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
406421
).toString()
407422
)
408423
val request = Request.Builder()
409-
.url("https://securetoken.googleapis.com/v1/token?key=" + app.options.apiKey)
424+
.url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token"))
410425
.post(body)
411426
.tag(REFRESH_TOKEN_TAG)
412427
.build()
@@ -568,5 +583,8 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
568583
fun signInWithEmailLink(email: String, link: String): Task<AuthResult> = TODO()
569584

570585
fun setLanguageCode(value: String): Nothing = TODO()
571-
fun useEmulator(host: String, port: Int): Unit = TODO()
586+
587+
fun useEmulator(host: String, port: Int) {
588+
urlFactory = UrlFactory(app, "http://$host:$port/")
589+
}
572590
}

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@example.com", "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@example.com", "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@example.com",
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@example.com",
21+
"federatedId": "email@example.com",
22+
"rawId": "email@example.com",
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)