From 9671836ddbc4ba7d9d461ebeb1eab8ea5234578d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Ma=C5=A1a?= Date: Mon, 7 Oct 2024 23:26:22 +0200 Subject: [PATCH] Test Auth against emulator --- .firebaserc | 1 + .github/workflows/build-pr.yml | 10 ++-- firebase.json | 11 +++++ .../com/google/firebase/auth/FirebaseAuth.kt | 35 ++++++++++---- src/test/kotlin/AuthTest.kt | 47 +++++++++++++++++++ src/test/kotlin/FirebaseTest.kt | 24 ++++++++++ src/test/kotlin/FirestoreTest.kt | 31 ++---------- .../firebase_data/auth_export/accounts.json | 29 ++++++++++++ .../firebase_data/auth_export/config.json | 8 ++++ .../firebase-export-metadata.json | 7 +++ 10 files changed, 164 insertions(+), 39 deletions(-) create mode 100644 .firebaserc create mode 100644 firebase.json create mode 100644 src/test/kotlin/AuthTest.kt create mode 100644 src/test/resources/firebase_data/auth_export/accounts.json create mode 100644 src/test/resources/firebase_data/auth_export/config.json create mode 100644 src/test/resources/firebase_data/firebase-export-metadata.json diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.firebaserc @@ -0,0 +1 @@ +{} diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 15b5ace..028ce50 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -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' diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..2fb2a16 --- /dev/null +++ b/firebase.json @@ -0,0 +1,11 @@ +{ + "emulators": { + "auth": { + "port": 9099 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index 3a41a23..ee3abd0 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -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 @@ -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 by lazy { @@ -85,7 +97,7 @@ class FirebaseUserImpl private constructor( val source = TaskCompletionSource() 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 { @@ -184,11 +196,13 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { } } + private var urlFactory = UrlFactory(app) + fun signInAnonymously(): Task { val source = TaskCompletionSource() 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 { @@ -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 { @@ -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 { @@ -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() @@ -439,5 +453,8 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { fun signInWithEmailLink(email: String, link: String): Task = 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/") + } } diff --git a/src/test/kotlin/AuthTest.kt b/src/test/kotlin/AuthTest.kt new file mode 100644 index 0000000..7bdb90c --- /dev/null +++ b/src/test/kotlin/AuthTest.kt @@ -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) + } +} diff --git a/src/test/kotlin/FirebaseTest.kt b/src/test/kotlin/FirebaseTest.kt index 224b473..77aa858 100644 --- a/src/test/kotlin/FirebaseTest.kt +++ b/src/test/kotlin/FirebaseTest.kt @@ -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() + 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() } } diff --git a/src/test/kotlin/FirestoreTest.kt b/src/test/kotlin/FirestoreTest.kt index 95afc48..f3a8113 100644 --- a/src/test/kotlin/FirestoreTest.kt +++ b/src/test/kotlin/FirestoreTest.kt @@ -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() - 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)) } diff --git a/src/test/resources/firebase_data/auth_export/accounts.json b/src/test/resources/firebase_data/auth_export/accounts.json new file mode 100644 index 0000000..3b70f06 --- /dev/null +++ b/src/test/resources/firebase_data/auth_export/accounts.json @@ -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": "" + } + ] + } + ] +} diff --git a/src/test/resources/firebase_data/auth_export/config.json b/src/test/resources/firebase_data/auth_export/config.json new file mode 100644 index 0000000..9e07e93 --- /dev/null +++ b/src/test/resources/firebase_data/auth_export/config.json @@ -0,0 +1,8 @@ +{ + "signIn": { + "allowDuplicateEmails": false + }, + "emailPrivacyConfig": { + "enableImprovedEmailPrivacy": false + } +} diff --git a/src/test/resources/firebase_data/firebase-export-metadata.json b/src/test/resources/firebase_data/firebase-export-metadata.json new file mode 100644 index 0000000..13a607f --- /dev/null +++ b/src/test/resources/firebase_data/firebase-export-metadata.json @@ -0,0 +1,7 @@ +{ + "version": "13.3.1", + "auth": { + "version": "13.3.1", + "path": "auth_export" + } +} \ No newline at end of file