Firestore Test Production Security Rules With Android Unit Test
March 7, 2019We shall test Firestore Security Rules in production environment using Android Unit Test (Instrumented tests).
- We will be using Email & Password authentication
- Need to pre-create users
NOTE: Other methods to test firestore security rules include Simulator and Firestore Emulator.
Android depedencies for testing.
dependencies {
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:core-ktx:1.1.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
NOTE: Assuming you already setup Firestore on Android.
Create FirestoreSecurityRulesTest.kt
at androidTest
.
- The following unit tests are to test against this User Security Rules.
- Use
@LargeTest
because of network request (Firestore backend) is involved. @FixMethodOrder(MethodSorters.NAME_ASCENDING)
because some test involved creating document and query by another test.expectFirestorePermissionDenied
is used to detect rejected Firestore Security Rules.- Pre-create 3 users (2 normal users, 1 admin) using email & password authentication.
- Assign admin role to one of the user.
- Create methods to switch user such as
signOut
,signInAsUser1
,signInAsUser2
andsignInAsAdmin
. setup
test by assigningdb: FirebaseFirestore
,auth: FirebaseAuth
anduser1: FirebaseUser
.- We shall use
user1
to create document, and useuser1
(owner),user2
(non-owner) andadmin
(admin) to test access permission tearUp
is used to delete document created during this test
To run the test, right click on FirestoreSecurityRulesTest
and select run 'FirestoreSecurityRulesTest'
.
NOTE: When I run the test, occasionally network is lost/disabled on my device, thus I created test01_network
to check for network issue. To solve this issue, launch the app (click the app icon on the device), then run the test again.
NOTE: This unit tests probably didn’t conver all cases, but good enough as a starter.
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.android.gms.tasks.Tasks
import com.google.firebase.Timestamp
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.FirebaseFirestoreSettings
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.greaterThan
import org.hamcrest.core.StringContains
import org.junit.*
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import java.net.URL
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import javax.net.ssl.HttpsURLConnection
import org.hamcrest.CoreMatchers.`is` as Is
@RunWith(AndroidJUnit4::class)
@LargeTest // need network
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class FirestoreSecurityRulesTest {
companion object {
private const val COLLECTION_USER = "users"
}
private lateinit var db: FirebaseFirestore
private lateinit var auth: FirebaseAuth
private lateinit var user1: FirebaseUser
@Rule
@JvmField
val expectedException = ExpectedException.none()
fun expectFirestorePermissionDenied() {
expectedException.expect(ExecutionException::class.java)
expectedException.expectCause(isA(FirebaseFirestoreException::class.java))
expectedException.expectMessage(StringContains("PERMISSION_DENIED"))
}
fun signInAsUser1(): FirebaseUser {
auth.signOut()
Tasks.await(auth.signInWithEmailAndPassword("user1@luasoftware.com", "***"))
assertThat(auth.currentUser, Is(notNullValue()))
assertThat(auth.currentUser?.email, Is("user1@luasoftware.com"))
return auth.currentUser!!
}
fun signInAsUser2(): FirebaseUser {
auth.signOut()
Tasks.await(auth.signInWithEmailAndPassword("user2@luasoftware.com", "***"))
assertThat(auth.currentUser, Is(notNullValue()))
assertThat(auth.currentUser?.email, Is("user2@luasoftware.com"))
return auth.currentUser!!
}
fun signInAsAdmin(): FirebaseUser {
auth.signOut()
Tasks.await(auth.signInWithEmailAndPassword("admin@luasoftware.com", "***"))
assertThat(auth.currentUser, Is(notNullValue()))
assertThat(auth.currentUser?.email, Is("admin@luasoftware.com"))
return auth.currentUser!!
}
fun signOut() {
auth.signOut()
assertThat(auth.currentUser, Is(nullValue()))
}
@Before
fun setup() {
db = FirebaseFirestore.getInstance()
val settings = FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(false)
.build()
db.firestoreSettings = settings
auth = FirebaseAuth.getInstance()
user1 = signInAsUser1()
}
@After
fun tearUp() {
signInAsAdmin()
val batch = db.batch()
val collection = db.collection(COLLECTION_USER)
batch.delete(collection.document(user1.uid))
// these documents shouldn't be created
batch.delete(collection.document("test_create_user_public"))
batch.delete(collection.document("test_create_user_fake_id"))
batch.commit()
}
/**
* Sometimes network connection is disabled when running test
*/
@Test
fun test01_network() {
val url = URL("https://www.android.com/")
val conn = url.openConnection() as HttpsURLConnection
assertThat(conn.responseCode, Is(HttpsURLConnection.HTTP_OK))
}
/**
* Public cannot create User document
*/
@Test
fun test02_createUserByPublic() {
signOut()
val task = db.collection(COLLECTION_USER)
.document("test_create_user_public")
.set(mapOf("roles" to listOf("user")))
expectFirestorePermissionDenied()
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* User can create User document
*/
@Test
fun test03_createUserAsUser1() {
val user = signInAsUser1()
val docRef = db.collection(COLLECTION_USER)
.document(user.uid)
val doc = Tasks.await(docRef.get())
// make sure it doesn't exist
assertThat(doc.exists(), Is(false))
val task = docRef
.set(mapOf(
"name" to "User1",
"roles" to listOf("user")
))
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* User cannot create User with FakeId
*/
@Test
fun test04_createUserWithFakeId() {
val user = signInAsUser1()
val docRef = db.collection(COLLECTION_USER)
.document("test_create_user_fake_id")
val doc = Tasks.await(docRef.get())
// make sure it doesn't exist
assertThat(doc.exists(), Is(false))
val task = docRef
.set(mapOf(
"name" to "User1",
"roles" to listOf("user")
))
expectFirestorePermissionDenied()
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* User cannot create User document with admin role
*/
@Test
fun test05_createUserWithAdminRole() {
val user = signInAsUser2()
val docRef = db.collection(COLLECTION_USER)
.document(user.uid)
val doc = Tasks.await(docRef.get())
// make sure it doesn't exist
assertThat(doc.exists(), Is(false))
val task = docRef
.set(mapOf(
"name" to "User2",
"roles" to listOf("user", "admin")
))
expectFirestorePermissionDenied()
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* Public can read/list any User document
*/
@Test
fun test06_readUserAsPublic() {
signOut()
val docRef = db.collection(COLLECTION_USER)
.document(user1.uid)
val doc = Tasks.await(docRef.get())
assertThat(doc.exists(), Is(true))
assertThat(doc.getString("name"), Is("User1"))
val task = db.collection(COLLECTION_USER)
.whereArrayContains("roles", "user")
.get()
val results = Tasks.await(task, 10, TimeUnit.SECONDS)
assertThat(results.size(), greaterThan(0))
}
/**
* Owner can update User document
*/
@Test
fun test07_updateUserAsOwner() {
val user = signInAsUser1()
val docRef = db.collection(COLLECTION_USER)
.document(user.uid)
val doc = Tasks.await(docRef.get())
assertThat(doc.exists(), Is(true))
val task = docRef
.update(mapOf(
"modified" to Timestamp.now()
))
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* Owner cannot change User document roles
*/
@Test
fun test08_updateUserRolesAsOwner() {
val user = signInAsUser1()
val docRef = db.collection(COLLECTION_USER)
.document(user.uid)
val doc = Tasks.await(docRef.get())
assertThat(doc.exists(), Is(true))
val task = docRef
.update(mapOf(
"roles" to listOf("user", "admin")
))
expectFirestorePermissionDenied()
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* Owner/User cannot delete User document
*/
fun test09_deleteUserAsOwner() {
val user = signInAsUser1()
val docRef = db.collection(COLLECTION_USER)
.document(user.uid)
val doc = Tasks.await(docRef.get())
assertThat(doc.exists(), Is(true))
val task = docRef.delete()
expectFirestorePermissionDenied()
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* Non-owner cannot update User document
*/
fun test10_updateUserAsNonOwner() {
val user = signInAsUser2()
val docRef = db.collection(COLLECTION_USER)
.document(user1.uid)
// make sure not owner
assertThat(docRef.id, Is(not(user.uid)))
val task = docRef.update(mapOf(
"name" to "cannot change name by non-owner"
))
expectFirestorePermissionDenied()
Tasks.await(task, 10, TimeUnit.SECONDS)
}
/**
* Admin can do anything
*/
fun test11_updateUserAsAdmin() {
val user = signInAsAdmin()
val docRef = db.collection(COLLECTION_USER)
.document(user1.uid)
// make sure not owner
assertThat(docRef.id, Is(not(user.uid)))
val task = docRef.update(mapOf(
"remark" to "changes by admin",
"roles" to listOf("user", "moderator")
))
}
}
- algo-trading
- algolia
- analytics
- android
- android-ktx
- android-permission
- android-studio
- apps-script
- bash
- binance
- bootstrap
- bootstrapvue
- chartjs
- chrome
- cloud-functions
- coding-interview
- contentresolver
- coroutines
- crashlytics
- crypto
- css
- dagger2
- datastore
- datetime
- docker
- eslint
- firebase
- firebase-auth
- firebase-hosting
- firestore
- firestore-security-rules
- flask
- fontawesome
- fresco
- git
- github
- glide
- godot
- google-app-engine
- google-cloud-storage
- google-colab
- google-drive
- google-maps
- google-places
- google-play
- google-sheets
- gradle
- html
- hugo
- inkscape
- java
- java-time
- javascript
- jetpack-compose
- jetson-nano
- kotlin
- kotlin-serialization
- layout
- lets-encrypt
- lifecycle
- linux
- logging
- lubuntu
- markdown
- mate
- material-design
- matplotlib
- md5
- mongodb
- moshi
- mplfinance
- mysql
- navigation
- nginx
- nodejs
- npm
- nuxtjs
- nvm
- pandas
- payment
- pip
- pwa
- pyenv
- python
- recylerview
- regex
- room
- rxjava
- scoped-storage
- selenium
- social-media
- ssh
- ssl
- static-site-generator
- static-website-hosting
- sublime-text
- ubuntu
- unit-test
- uwsgi
- viewmodel
- viewpager2
- virtualbox
- vue-chartjs
- vue-cli
- vue-router
- vuejs
- vuelidate
- vuepress
- web-development
- web-hosting
- webpack
- windows
- workmanager
- wsl
- yarn