How to Emulate Firebase Auth
Tags: javascript, webdev, firebase, testing
Published: 2021-10-24 01:26:27

I was recently building an app in which I was trying to integrate Firebase Authentication and Firebase Realtime Database. But I ran into a problem pretty quickly while I was testing things locally.

Even though Firebase has a super amazing Emulator Suite for local testing, authentication is not included. To me, this meant that the lovely auth-based Realtime DB rules I'd crafted were impossible to test locally unless I modified my DB rules beforehand. But that doesn't make for a very good permissions test, does it? There is an open issue on [GitHub](https://github.com/firebase/firebase-tools/issues/1677) for addressing this, but at the time of writing, no solution has yet been included in the emulator suite.

> **Update 2020-10-27:** Firebase Auth is now part of the Emulator Suite! Upgrade the Firebase CLI to version 8.14.0 or greater to use it. If that's all you were looking for, the rest of this post might not be useful to you, but feel free to keep reading for a more detailed look at Firebase Auth and my general philosophy towards testing and modularization.

I spent a bunch of hours trying to figure out how to string things together with Band-Aids and glue to do something that honestly seems like a pretty basic requirement for DB testing: Test my auth rules in both development and production without modifying the very security model I'm trying to test. After all, who would want to do "real" permissions testing for the first time in a production environment??

Nothing was working. I was stuck. I missed. [Then I missed again. Then I got sad. I had a popsicle. And then I passed out in the snow.](https://youtu.be/02WuLTpe3sI?t=355)

Just kidding on the last few, but what I *did* do what have an epiphany in the shower. I do some of my best thinking there. *Anyone else? No? Okay. Moving on.*

## The Solution

My app in particular is using Google Login and the Google auth provider, so that's what I'm going to focus on here, but I believe this approach would translate to other auth providers as well.

The key to making this work is abstraction. Take any Firebase call that you'd normally make and hide it behind a function that may or may not do the same thing. Usually, it's the same sort of thing with some extras sprinkled in.

In this case, we'll be looking at the `firebase.initializeApp` function. In the normal production environment, this is super simple. We pass in a `siteConfig` object and we're on our merry way. However, when working locally and/or with Firebase Emulators, this doesn't work one-for-one. In [the docs](https://firebase.google.com/docs/rules/unit-tests), they indicate that we should use `initializeTestApp` instead to perform our initialization. This comes from the `@firebase/testing` module as opposed to the `firebase/app` module. This might seem perfect on the surface, but the issue is that anywhere we might normally use `firebase.<sometThing>` to interact with the default firebase app, we can't. We instead need to work with the app instance returned from the call to `firebase.initializeTestApp()`. By extension, this means we should structure our code so that we're *always* using `app.<someThing>` in favor of `firebase.<someThing>`, regardless of whether we're using `initializeApp` or `initializeTestApp`.

Again, this doesn't seem too bad on the surface, but there's one more catch: In each case, the app instance (as provided by `initialize*App(siteConfig)`) is slightly different. Namely, `app.auth()` is not a thing for apps initialized via `initializeTestApp()`.

This is the crux of the auth emulation problem. And this is what we are going to solve. Let's take a look at some code.

Here is a utility function to initialize either a test or production app and return it:

```js
const createApp = async (onAuthStateChanged) => {
    const firebase = await importFirebase()

    if (isDevelopment) {
        const app = firebase.initializeTestApp(siteConfig)

        // set up custom hooks for auth mocking
        app.__internal__ = {
            onAuthStateChanged
        }

        return app
    } else {
        const app = firebase.initializeApp(siteConfig)

        // Set up the auth observer
        app.auth().onAuthStateChanged(onAuthStateChanged)

        return app;
    }
}
```

There's a lot going on here, so let's break it down line by line.

<br />

```js
const createApp = async (onAuthStateChanged) => {
```

I went with `async` here because, in a couple lines, you'll see some dynamic imports. More on that in a sec. The other important piece here is that this `createApp` function takes an `onAuthStateChanged` callback and not a `siteConfig` object like `initializeApp`. Since we control the module containing this abstraction function, we can put our `siteConfig` object here too for easy access. I mean, you can put the `siteConfig` wherever you want, but to me, it makes sense to have the same module own the config block and the utility functions since the goal is to drive all Firebase-related functions through this abstraction module.

The `onAuthStateChanged` callback will be called when—*you guessed it*—the auth state changes. In the production case, we can simply set up an auth observer in the usual manner, but in the development case, it's a bit more interesting. More on that in a sec.

```js
const firebase = await importFirebase()
```

Here's another layer of abstraction. We want a reference to Firebase as a module, and more specifically we might want a reference to the "testing" version of Firebase, but we don't actually care how it is obtained. Dynamic imports are a huge help here. This is what the definition of `importFirebase` looks like:

```js
const importFirebase = async () => {
    if (isDevelopment) {
        return await import('@firebase/testing')
    } else {
        const firebase = await import('firebase/app')

        await import('firebase/auth')
        await import('firebase/database')

        return firebase
    }
}
```

There's nothing too surprising here. We are either importing "test" Firebase from `@firebase/testing` or we are importing "real" Firebase from `firebase/app` along with our other Firebase dependencies. Dynamically importing "real" Firebase is a little more involved, but it's basically the traditional way of doing it converted to dynamic import-form.

I feel like this is a good time to mention that the reason for using dynamic imports here is so that you only ever end up importing *either* the test Firebase *or* the production one, but never both. Dynamic imports give us that flexibility.

> The astute reader might realize that even with dynamic imports, Webpack will still bundle both modules into the output since we don't know until runtime which type of environment we'll be in. While this is true, it can be avoided by splitting the vendor modules out as part of the build and filtering out one of the two Firebase chunks, depending on the build type.

## Development Mode

```js
if (isDevelopment) {
```

Assuming this is a React app created via `create-react-app`, we can calculate whether or not this is a development build by looking for `process.env.NODE_ENV === 'development'`

<br />

```js
const app = firebase.initializeTestApp(siteConfig)
```

Next, we need to initialize the test app using the now-obtained Firebase module, providing it our `siteConfig` as usual. There's a key piece that needs to exist in the `siteConfig` for this to work though: An `auth` block. Here's an example config:

```js
const siteConfig = {
    apiKey: '...',
    authDomain: window.location.host,
    databaseURL: isDevelopment
        ? 'http://localhost:9000?ns=...'
        : 'https://....firebaseio.com',
    databaseName: '...',
    projectId: '...',
    storageBucket: '....appspot.com',
    messagingSenderId: '...',
    appId: '...',
    measurementId: '...',
    auth: {
        uid: 'u111111',
        email: 'u111111@example.com'
    }
}

```

That auth block is the key because that means that we can "inject" a user/email into the app manually as we see fit. There's a caveat though... Since this isn't *real* auth, we'll never get `onAuthStateChanged` callbacks fired. We're going to need to do that ourselves. And the first step towards doing that is to store a reference to the provided callback in our test app for later:

```js
// set up custom hooks for auth mocking
app.__internal__ = {
    onAuthStateChanged
}

return app
```

> Here I chose `__internal__` as a namespace that I figured nobody would collide with, but this could just as easily have been any other unique key on the app object.

## Production Mode

The other case to consider here is the production case. Let's take a look at the `else` block:

```js
} else {
    const app = firebase.initializeApp(siteConfig)

    // Set up the auth observer
    app.auth().onAuthStateChanged(onAuthStateChanged)

    return app;
}
```

This is very similar to what happens in development except we end up importing "real" Firebase and setting up an actual auth observer with that callback we took in as an argument.

All of this is to say that we can now call

```js
const app = MyFirebaseUtils.createApp(onAuthStateChanged)
```

to get back a firebase app that's ready to go with either emulated auth in development or real auth in production.

> I recommend holding onto this app instance in your application state so that it can be provided to any abstraction functions that may depend on it, such as simulating a login in development mode.

## Simulating onAuthStateChanged

For any function we have that would trigger a login (or logout), we can add in a separate development-only flow in which we manually fire an `onAuthStateChanged` event. Looking at the [docs](https://firebase.google.com/docs/reference/js/firebase.auth.Auth#onauthstatechanged), those events are either passed a user or `null` depending on whether the user is logged in or not.

If our production flow for logging in a user looks like this:

```js
const doGoogleLogin = async (app, onSuccess, onFailure) => {
    const firebase = await importFirebase()
    const provider = new firebase.auth.GoogleAuthProvider()

    // Show the actual login popup. Succeeding here will update the internally managed uid and
    // auth of the app, which allows subsequent database calls (and other stuff) to work.
    app.auth().signInWithPopup(provider)
        .then(onSuccess)
        .catch(onFailure)
}
```

Then we can add in a development flow, like this:

```js
const doGoogleLogin = async (app, onSuccess, onFailure) => {
    if (isDevelopment) {
        // https://firebase.google.com/docs/reference/js/firebase.auth#usercredential
        onSuccess({
            credential: {
                accessToken: TEST_ID_AUTH_TOKEN
            },
            user: {
                uid: siteConfig.auth.uid
            }
        })

        // Fire a simulated onAuthStateChanged event, passing it the user from our siteConfig.auth block
        app.__internal__.onAuthStateChanged({
            uid: siteConfig.auth.uid,
            getIdToken: () => (TEST_ID_AUTH_TOKEN)
        })
    } else {
        // production flow
    }
}
```

And there you have it! A sorta-kinda way to emulate auth from within a Firebase-enabled app. Hopefully you find this useful. I've been successfully using this approach in my project to help with offline testing using Firebase emulators.