Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth: Add support for emulator #38

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
10 changes: 7 additions & 3 deletions .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ jobs:
with:
distribution: 'zulu'
java-version: 17
- name: Build
uses: eskatos/gradle-command-action@v3
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
arguments: build
node-version: 20
- name: Install Firebase CLI
run: npm install -g firebase-tools
- name: Build
run: firebase emulators:exec --project my-firebase-project --import=src/test/resources/firebase_data './gradlew build'
11 changes: 11 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"emulators": {
"auth": {
"port": 9099
},
"ui": {
"enabled": true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really necessary for CI, but it's useful when developing.

},
"singleProjectMode": true
}
}
35 changes: 26 additions & 9 deletions src/main/java/com/google/firebase/auth/FirebaseAuth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ import java.util.concurrent.TimeUnit

val jsonParser = Json { ignoreUnknownKeys = true }

class UrlFactory(
private val app: FirebaseApp,
private val emulatorUrl: String? = null
) {
fun buildUrl(uri: String): String {
return "${emulatorUrl ?: "https://"}$uri?key=${app.options.apiKey}"
}
}

@Serializable
class FirebaseUserImpl private constructor(
@Transient
Expand All @@ -52,17 +61,20 @@ class FirebaseUserImpl private constructor(
val idToken: String,
val refreshToken: String,
val expiresIn: Int,
val createdAt: Long
val createdAt: Long,
@Transient
private val urlFactory: UrlFactory = UrlFactory(app)
) : FirebaseUser() {

constructor(app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false) : this(
constructor(app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, urlFactory: UrlFactory = UrlFactory(app)) : this(
app,
isAnonymous,
data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "",
data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content,
data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content,
data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int,
data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis()
data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis(),
urlFactory
)

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

private var urlFactory = UrlFactory(app)

fun signInAnonymously(): Task<AuthResult> {
val source = TaskCompletionSource<AuthResult>()
val body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString())
val request = Request.Builder()
.url("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=" + app.options.apiKey)
.url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:signUp"))
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
Expand Down Expand Up @@ -220,7 +234,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString()
)
val request = Request.Builder()
.url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=" + app.options.apiKey)
.url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"))
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
Expand Down Expand Up @@ -252,7 +266,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
JsonObject(mapOf("email" to JsonPrimitive(email), "password" to JsonPrimitive(password), "returnSecureToken" to JsonPrimitive(true))).toString()
)
val request = Request.Builder()
.url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=" + app.options.apiKey)
.url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword"))
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
Expand Down Expand Up @@ -336,7 +350,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
).toString()
)
val request = Request.Builder()
.url("https://securetoken.googleapis.com/v1/token?key=" + app.options.apiKey)
.url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token"))
.post(body)
.build()

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

fun setLanguageCode(value: String): Nothing = TODO()
fun useEmulator(host: String, port: Int): Unit = TODO()

fun useEmulator(host: String, port: Int) {
urlFactory = UrlFactory(app, "http://$host:$port/")
}
}
47 changes: 47 additions & 0 deletions src/test/kotlin/AuthTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuthInvalidUserException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test

class AuthTest : FirebaseTest() {
private fun createAuth(): FirebaseAuth {
return FirebaseAuth(app).apply {
useEmulator("localhost", 9099)
}
}

@Test
fun `should authenticate via anonymous auth`() = runTest {
val auth = createAuth()

auth.signInAnonymously().await()

assertEquals(true, auth.currentUser?.isAnonymous)
}

@Test
fun `should authenticate via email and password`() = runTest {
val auth = createAuth()

auth.signInWithEmailAndPassword("email@example.com", "securepassword").await()

assertEquals(false, auth.currentUser?.isAnonymous)
}

@Test
fun `should throw exception on invalid password`() {
val auth = createAuth()

val exception = assertThrows(FirebaseAuthInvalidUserException::class.java) {
runBlocking {
auth.signInWithEmailAndPassword("email@example.com", "wrongpassword").await()
}
}

assertEquals("INVALID_PASSWORD", exception.errorCode)
}
}
24 changes: 24 additions & 0 deletions src/test/kotlin/FirebaseTest.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import android.app.Application
import com.google.firebase.Firebase
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.FirebasePlatform
import com.google.firebase.initialize
import org.junit.Before
import java.io.File

abstract class FirebaseTest {
protected val app: FirebaseApp get() {
val options = FirebaseOptions.Builder()
.setProjectId("my-firebase-project")
.setApplicationId("1:27992087142:android:ce3b6448250083d1")
.setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw")
.build()

return Firebase.initialize(Application(), options)
}

@Before
fun beforeEach() {
FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() {
val storage = mutableMapOf<String, String>()
override fun store(key: String, value: String) = storage.set(key, value)
override fun retrieve(key: String) = storage[key]
override fun clear(key: String) { storage.remove(key) }
override fun log(msg: String) = println(msg)
override fun getDatabasePath(name: String) = File("./build/$name")
})
FirebaseApp.clearInstancesForTest()
}
}
31 changes: 4 additions & 27 deletions src/test/kotlin/FirestoreTest.kt
Original file line number Diff line number Diff line change
@@ -1,42 +1,19 @@
import android.app.Application
import com.google.firebase.Firebase
import com.google.firebase.FirebaseOptions
import com.google.firebase.FirebasePlatform
import com.google.firebase.firestore.firestore
import com.google.firebase.initialize
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.io.File

class FirestoreTest : FirebaseTest() {
@Before
fun initialize() {
FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() {
val storage = mutableMapOf<String, String>()
override fun store(key: String, value: String) = storage.set(key, value)
override fun retrieve(key: String) = storage[key]
override fun clear(key: String) { storage.remove(key) }
override fun log(msg: String) = println(msg)
override fun getDatabasePath(name: String) = File("./build/$name")
})
val options = FirebaseOptions.Builder()
.setProjectId("my-firebase-project")
.setApplicationId("1:27992087142:android:ce3b6448250083d1")
.setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw")
// setDatabaseURL(...)
// setStorageBucket(...)
.build()
Firebase.initialize(Application(), options)
Firebase.firestore.disableNetwork()
}

@Test
fun testFirestore(): Unit = runTest {
val firestore = Firebase.firestore(app)
firestore.disableNetwork().await()

val data = Data("jim")
val doc = Firebase.firestore.document("sally/jim")
val doc = firestore.document("sally/jim")
doc.set(data)
assertEquals(data, doc.get().await().toObject(Data::class.java))
}
Expand Down
29 changes: 29 additions & 0 deletions src/test/resources/firebase_data/auth_export/accounts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"kind": "identitytoolkit#DownloadAccountResponse",
"users": [
{
"localId": "Ijat10t0F1gvH1VrClkkSqEcId1p",
"lastLoginAt": "1728509249920",
"displayName": "",
"photoUrl": "",
"emailVerified": true,
"email": "email@example.com",
"salt": "fakeSaltHsRxYqy9iKVQRLwz8975",
"passwordHash": "fakeHash:salt=fakeSaltHsRxYqy9iKVQRLwz8975:password=securepassword",
"passwordUpdatedAt": 1728509249921,
"validSince": "1728509249",
"mfaInfo": [],
"createdAt": "1728509249920",
"providerUserInfo": [
{
"providerId": "password",
"email": "email@example.com",
"federatedId": "email@example.com",
"rawId": "email@example.com",
"displayName": "",
"photoUrl": ""
}
]
}
]
}
8 changes: 8 additions & 0 deletions src/test/resources/firebase_data/auth_export/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"signIn": {
"allowDuplicateEmails": false
},
"emailPrivacyConfig": {
"enableImprovedEmailPrivacy": false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": "13.3.1",
"auth": {
"version": "13.3.1",
"path": "auth_export"
}
}