Repository: firebase/firebase-tools
Pull request #1459 (2019-07-02 17:43:23)

Refactor Firebase Admin Stubbing
<!--

Thank you for contributing to the Firebase community! Please fill out the form below.

Run the linter and test suite
==============================
Run `npm test` to make sure your changes compile properly and the tests all pass on your local machine. We've hooked up this repo with continuous integration to double check those things for you. 

-->


### Description

Fixes issue #1454
	 
This investigation took me very deep and along the way I also:

  * Added some standardization for how we resolve developer node modules and made sure we log them.
  * Simplified some of our logging code.

### Scenarios Tested

Using these functions to test:

```js
const functions = require("firebase-functions");
const admin = require('firebase-admin');

const app = admin.initializeApp();

exports.writeThroughAdmin = functions.https.onRequest(async (req, res) => {
    const firestore = admin.firestore();
    const ref = firestore.doc("info/test");
    await ref.set({ hello: (new Date().toISOString()) });
    ref.get().then((snap) => {
        res.json({ data: snap.data() });
    });
});

exports.writeThroughApp = functions.https.onRequest(async (req, res) => {
    const firestore = app.firestore();
    const ref = firestore.doc("info/test");
    await ref.set({ hello: (new Date().toISOString()) });
    ref.get().then((snap) => {
        res.json({ data: snap.data() });
    });
});

exports.firestoreReaction = functions.firestore.document("info/test").onWrite(async (change, ctx) => {
    console.log('~~~~~~~~~~ FIRESTORE WRITE ~~~~~~~~~~');
    return true;
});
```

I also added a unit test that verifies the same behavior.

### Sample Commands

N/A

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24330794/badge)](https://coveralls.io/builds/24330794)

Coverage increased (+0.1%) to 62.116% when pulling **425576c069731a29f62bb28e0d6c9af6dffab340 on ss-fix-1454** into **c19ff153c939b77384851d74d540896958af5d40 on master**.

samtstern: @yuchenshi thanks for taking a look.  I agree with waiting for @abeisgoat 

Patch:
diff --git a/changelog.txt b/changelog.txt
index 35526402f..e10828e7c 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -9,3 +9,4 @@
 * Update functions init templates to `v3.1.0`
 * Fix emulation of https functions.
 * Fix bug where calling `clearFirestoreData` against the Firestore Emulator fails if more than 500 documents exist.
+* Fixes a bug where `admin.firestore()` and `app.firestore()` behaved differently.
diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts
index 1520e70a0..98a8f86cc 100644
--- a/src/emulator/functionsEmulator.ts
+++ b/src/emulator/functionsEmulator.ts
@@ -387,7 +387,11 @@ You can probably fix this by running "npm install ${
         EmulatorLogger.log("USER", `${clc.blackBright("> ")} ${log.text}`);
         break;
       case "DEBUG":
-        EmulatorLogger.log("DEBUG", log.text);
+        if (log.data && log.data !== {}) {
+          EmulatorLogger.log("DEBUG", `[${log.type}] ${log.text} ${JSON.stringify(log.data)}`);
+        } else {
+          EmulatorLogger.log("DEBUG", `[${log.type}] ${log.text}`);
+        }
         break;
       case "INFO":
         EmulatorLogger.logLabeled("BULLET", "functions", log.text);
diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts
index 8cf299b2c..4d70a4fe6 100644
--- a/src/emulator/functionsEmulatorRuntime.ts
+++ b/src/emulator/functionsEmulatorRuntime.ts
@@ -17,8 +17,11 @@ import * as bodyParser from "body-parser";
 import { URL } from "url";
 import * as _ from "lodash";
 
-let app: admin.app.App;
+let defaultApp: admin.app.App;
 let adminModuleProxy: typeof admin;
+let hasInitializedFirestore = false;
+
+let developerPkgJSON: PackageJSON | undefined;
 
 function isFeatureEnabled(
   frb: FunctionsRuntimeBundle,
@@ -42,14 +45,6 @@ async function requireResolveAsync(
   return require.resolve(moduleName, opts);
 }
 
-function isConstructor(obj: any): boolean {
-  return !!obj.prototype && !!obj.prototype.constructor.name;
-}
-
-function isExists(obj: any): boolean {
-  return obj !== undefined;
-}
-
 /**
  * See admin.credential.Credential.
  */
@@ -78,6 +73,7 @@ interface ModuleResolution {
   declared: boolean;
   installed: boolean;
   version?: string;
+  resolution?: string;
 }
 
 interface ModuleVersion {
@@ -86,6 +82,10 @@ interface ModuleVersion {
   patch: number;
 }
 
+interface ProxyTarget extends Object {
+  [key: string]: any;
+}
+
 /*
   This helper is used to create mocks for Firebase SDKs. It simplifies creation of Proxy objects
   by allowing us to easily overide some or all of an objects methods. When placed back into require's
@@ -99,21 +99,21 @@ interface ModuleVersion {
   obj.value == 1;
   obj.incremented == 2;
    */
-class Proxied<T> {
-  proxy: any;
-  private anyValue?: (target: any, key: string) => any;
+class Proxied<T extends ProxyTarget> {
+  proxy: T;
+  private anyValue?: (target: T, key: string) => any;
   private appliedValue?: () => any;
   private rewrites: {
-    [key: string]: (target: any, key: string) => any;
+    [key: string]: (target: T, key: string) => any;
   } = {};
 
-  constructor(private original: any) {
-    /*
-      When initialized an original object is passed. This object is supplied to both .when()
-      and .any() functions so the original value of the object is accessible. When no
-      .any() is provided, the original value of the object is returned when the field
-      key does not match any known rewrite.
-      */
+  /**
+   * When initialized an original object is passed. This object is supplied to both .when()
+   * and .any() functions so the original value of the object is accessible. When no
+   * .any() is provided, the original value of the object is returned when the field
+   * key does not match any known rewrite.
+   */
+  constructor(private original: T) {
     this.proxy = new Proxy(this.original, {
       get: (target, key) => {
         key = key.toString();
@@ -137,56 +137,78 @@ class Proxied<T> {
     });
   }
 
-  when(key: string, value: (target: any, key: string) => any): Proxied<T> {
-    /*
-      Calling .when("a", () => "b") will rewrite obj["a"] to be equal to "b"
-       */
+  /**
+   * Calling .when("a", () => "b") will rewrite obj["a"] to be equal to "b"
+   */
+  when(key: string, value: (target: T, key: string) => any): Proxied<T> {
     this.rewrites[key] = value;
     return this as Proxied<T>;
   }
 
-  any(value: (target: any, key: string) => any): Proxied<T> {
-    /*
-      Calling .any(() => "b") will rewrite all fields on obj to be equal to "b"
-       */
+  /**
+   * Calling .any(() => "b") will rewrite all fields on obj to be equal to "b"
+   */
+  any(value: (target: T, key: string) => any): Proxied<T> {
     this.anyValue = value;
     return this as Proxied<T>;
   }
 
+  /**
+   * Calling .applied(() => "b") will make obj() equal to "b"
+   */
   applied(value: () => any): Proxied<T> {
-    /*
-      Calling .applied(() => "b") will make obj() equal to "b"
-       */
     this.appliedValue = value;
     return this as Proxied<T>;
   }
 
-  getOriginal(target: any, key: string): any {
+  /**
+   * Gets a property from the original object.
+   */
+  getOriginal(target: T, key: string): any {
     const value = target[key];
 
-    if (!isExists(value)) {
+    if (!this.isExists(value)) {
       return undefined;
-    } else if (isConstructor(value) || typeof value !== "function") {
+    } else if (this.isConstructor(value) || typeof value !== "function") {
       return value;
     } else {
       return value.bind(target);
     }
   }
 
+  /**
+   * Run the original target.
+   */
   applyOriginal(target: any, thisArg: any, argArray: any[]): any {
     return target.apply(thisArg, argArray);
   }
 
+  /**
+   * Return the final proxied object.
+   */
   finalize(): T {
     return this.proxy as T;
   }
+
+  private isConstructor(obj: any): boolean {
+    return !!obj.prototype && !!obj.prototype.constructor.name;
+  }
+
+  private isExists(obj: any): boolean {
+    return obj !== undefined;
+  }
 }
 
 async function resolveDeveloperNodeModule(
   frb: FunctionsRuntimeBundle,
-  pkg: PackageJSON,
   name: string
 ): Promise<ModuleResolution> {
+  const pkg = requirePackageJson(frb);
+  if (!pkg) {
+    new EmulatorLog("SYSTEM", "missing-package-json", "").log();
+    throw new Error("Could not find package.json");
+  }
+
   const dependencies = pkg.dependencies;
   const devDependencies = pkg.devDependencies;
   const isInPackageJSON = dependencies[name] || devDependencies[name];
@@ -197,35 +219,32 @@ async function resolveDeveloperNodeModule(
   }
 
   // Once we know it's in the package.json, make sure it's actually `npm install`ed
-  const modResolution = await requireResolveAsync(name, { paths: [frb.cwd] }).catch(NoOp);
-
-  if (!modResolution) {
+  const resolveResult = await requireResolveAsync(name, { paths: [frb.cwd] }).catch(NoOp);
+  if (!resolveResult) {
     return { declared: true, installed: false };
   }
 
-  const modPackageJSON = require(path.join(findModuleRoot(name, modResolution), "package.json"));
+  const modPackageJSON = require(path.join(findModuleRoot(name, resolveResult), "package.json"));
 
-  return {
+  const moduleResolution: ModuleResolution = {
     declared: true,
     installed: true,
     version: modPackageJSON.version,
+    resolution: resolveResult,
   };
+
+  logDebug(`Resolved module ${name}`, moduleResolution);
+  return moduleResolution;
 }
 
 async function verifyDeveloperNodeModules(frb: FunctionsRuntimeBundle): Promise<boolean> {
-  const pkg = requirePackageJson(frb);
-  if (!pkg) {
-    new EmulatorLog("SYSTEM", "missing-package-json", "").log();
-    return false;
-  }
-
   const modBundles = [
     { name: "firebase-admin", isDev: false, minVersion: 8 },
     { name: "firebase-functions", isDev: false, minVersion: 3 },
   ];
 
   for (const modBundle of modBundles) {
-    const resolution = await resolveDeveloperNodeModule(frb, pkg, modBundle.name);
+    const resolution = await resolveDeveloperNodeModule(frb, modBundle.name);
 
     /*
     If there's no reference to the module in their package.json, prompt them to install it
@@ -254,12 +273,17 @@ async function verifyDeveloperNodeModules(frb: FunctionsRuntimeBundle): Promise<
  * Get the developer's package.json file.
  */
 function requirePackageJson(frb: FunctionsRuntimeBundle): PackageJSON | undefined {
+  if (developerPkgJSON) {
+    return developerPkgJSON;
+  }
+
   try {
     const pkg = require(`${frb.cwd}/package.json`);
-    return {
+    developerPkgJSON = {
       dependencies: pkg.dependencies || {},
       devDependencies: pkg.devDependencies || {},
     };
+    return developerPkgJSON;
   } catch (err) {
     return undefined;
   }
@@ -320,13 +344,11 @@ function InitializeNetworkFiltering(frb: FunctionsRuntimeBundle): void {
     };
 
     networkingModules.push(gaxModule);
-    new EmulatorLog("DEBUG", "runtime-status", `Found google-gax at ${gaxPath}`).log();
+    logDebug(`Found google-gax at ${gaxPath}`);
   } catch (err) {
-    new EmulatorLog(
-      "DEBUG",
-      "runtime-status",
+    logDebug(
       `Couldn't find @google-cloud/firestore or google-gax, this may be okay if using @google-cloud/firestore@2.0.0`
-    ).log();
+    );
   }
 
   const history: { [href: string]: boolean } = {};
@@ -394,7 +416,7 @@ function InitializeNetworkFiltering(frb: FunctionsRuntimeBundle): void {
     return { name: bundle.name, status: "mocked" };
   });
 
-  new EmulatorLog("DEBUG", "runtime-status", "Outgoing network have been stubbed.", results).log();
+  logDebug("Outgoing network have been stubbed.", results);
 }
 /*
     This stub handles a very specific use-case, when a developer (incorrectly) provides a HTTPS handler
@@ -409,22 +431,19 @@ function InitializeNetworkFiltering(frb: FunctionsRuntimeBundle): void {
 https://github.com/firebase/firebase-functions/blob/9e3bda13565454543b4c7b2fd10fb627a6a3ab97/src/providers/https.ts#L66
    */
 async function InitializeFirebaseFunctionsStubs(frb: FunctionsRuntimeBundle): Promise<void> {
-  const pkgJSON = await requirePackageJson(frb);
+  const firebaseFunctionsResolution = await resolveDeveloperNodeModule(frb, "firebase-functions");
+  if (!firebaseFunctionsResolution.resolution) {
+    throw new Error("Could not resolve 'firebase-functions'");
+  }
 
-  // TODO: Simplify after #1459
-  const firebaseFunctionsModule = await resolveDeveloperNodeModule(
-    frb,
-    pkgJSON!,
-    "firebase-functions"
+  const firebaseFunctionsRoot = findModuleRoot(
+    "firebase-functions",
+    firebaseFunctionsResolution.resolution
   );
-  const firebaseFunctionsResolution = await requireResolveAsync("firebase-functions", {
-    paths: [frb.cwd],
-  });
-  const firebaseFunctionsRoot = findModuleRoot("firebase-functions", firebaseFunctionsResolution);
   const httpsProviderResolution = path.join(firebaseFunctionsRoot, "lib/providers/https");
 
   // TODO: Remove this logic and stop relying on internal APIs.  See #1480 for reasoning.
-  const functionsVersion = parseVersionString(firebaseFunctionsModule.version!);
+  const functionsVersion = parseVersionString(firebaseFunctionsResolution.version!);
   let methodName = "_onRequestWithOpts";
   if (functionsVersion.major >= 3 && functionsVersion.minor >= 1) {
     methodName = "_onRequestWithOptions";
@@ -492,62 +511,26 @@ async function getGRPCInsecureCredential(frb: FunctionsRuntimeBundle): Promise<a
     but it's hard to catch and better than accidentally talking to prod.
    */
 async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promise<typeof admin> {
-  const adminResolution = await requireResolveAsync("firebase-admin", { paths: [frb.cwd] });
-  const localAdminModule = require(adminResolution);
+  const adminResolution = await resolveDeveloperNodeModule(frb, "firebase-admin");
+  if (!adminResolution.resolution) {
+    throw new Error("Could not resolve 'firebase-admin'");
+  }
 
-  let hasInitializedSettings = false;
-  /*
-  If we can't get sslCreds that means either grpc or grpc-js doesn't exist. If this is the save,
-  then there's probably something really wrong (like a failed node-gyp build). If that's the case
-  we should silently fail here and allow the error to raise in user-code so they can debug appropriately.
-   */
+  // If we can't get sslCreds that means either grpc or grpc-js doesn't exist. If this is the save,
+  // then there's probably something really wrong (like a failed node-gyp build). If that's the case
+  // we should silently fail here and allow the error to raise in user-code so they can debug appropriately.
   const sslCreds = await getGRPCInsecureCredential(frb).catch(NoOp);
-  const initializeSettings = (userSettings: any) => {
-    const isEnabled = isFeatureEnabled(frb, "admin_stubs");
 
-    if (!isEnabled) {
-      if (!hasInitializedSettings) {
-        app.firestore().settings(userSettings);
-        hasInitializedSettings = true;
-      }
-      return;
-    }
-
-    if (!hasInitializedSettings && frb.ports.firestore) {
-      app.firestore().settings({
-        projectId: frb.projectId,
-        port: frb.ports.firestore,
-        servicePath: "localhost",
-        service: "firestore.googleapis.com",
-        sslCreds,
-        customHeaders: {
-          Authorization: "Bearer owner",
-        },
-        ...userSettings,
-      });
-    } else if (!frb.ports.firestore && frb.triggerId) {
-      new EmulatorLog(
-        "WARN",
-        "runtime-status",
-        "The Cloud Firestore emulator is not running so database operations will fail with a " +
-          "'default credentials' error."
-      ).log();
-    }
-
-    hasInitializedSettings = true;
-  };
+  const localAdminModule = require(adminResolution.resolution);
 
   adminModuleProxy = new Proxied<typeof admin>(localAdminModule)
-    .when("initializeApp", (adminModuleTarget) => (opts: any, appName: any) => {
+    .when("initializeApp", (adminModuleTarget) => (opts?: admin.AppOptions, appName?: string) => {
       if (appName) {
         new EmulatorLog("SYSTEM", "non-default-admin-app-used", "", { appName }).log();
         return adminModuleTarget.initializeApp(opts, appName);
       }
 
       const config = JSON.parse(process.env.FIREBASE_CONFIG || "{}");
-      new EmulatorLog("SYSTEM", "default-admin-app-used", `config=${config}`).log();
-
-      // TODO: Is there any possible harm in this?
       config.credential = makeFakeCredentials();
 
       if (frb.ports.database) {
@@ -555,42 +538,111 @@ async function InitializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis
         new EmulatorLog("SYSTEM", `Overriding database URL: ${config.databaseURL}`, "").log();
       }
 
-      app = adminModuleTarget.initializeApp({
+      const appOptions = {
         ...config,
         ...opts,
-      });
-      return app;
+      };
+      const originalApp = adminModuleTarget.initializeApp(appOptions);
+
+      new EmulatorLog("DEBUG", "default-admin-app-used", "", appOptions).log();
+      logDebug("Intializing default app.", appOptions);
+      defaultApp = proxyFirebaseApp(frb, originalApp, sslCreds);
+      return defaultApp;
     })
     .when("firestore", (adminModuleTarget) => {
-      const proxied = new Proxied<typeof admin.firestore>(adminModuleTarget.firestore);
-      return proxied
-        .applied(() => {
-          return new Proxied(adminModuleTarget.firestore())
-            .when("settings", () => {
-              return (settings: any) => {
-                initializeSettings(settings);
-              };
-            })
-            .any((target, field) => {
-              initializeSettings({});
-              return proxied.getOriginal(target, field);
-            })
-            .finalize();
-        })
-        .finalize();
+      // When we call admin.firestore() we want to forward this on to the default app,
+      // but if you access something like admin.firestore.FieldValue we don't want to
+      // do anything.
+      const adminFirestoreProxy = new Proxied<typeof admin.firestore>(
+        adminModuleTarget.firestore
+      ).applied(() => {
+        return defaultApp.firestore();
+      });
+
+      return adminFirestoreProxy.finalize();
     })
     .finalize();
 
-  require.cache[adminResolution] = {
+  // Stub the admin module in the require cache
+  require.cache[adminResolution.resolution] = {
     exports: adminModuleProxy,
   };
 
-  new EmulatorLog("DEBUG", "runtime-status", "firebase-admin has been stubbed.", {
+  logDebug("firebase-admin has been stubbed.", {
     adminResolution,
-  }).log();
+  });
+
   return adminModuleProxy;
 }
 
+function proxyFirebaseApp(
+  frb: FunctionsRuntimeBundle,
+  original: admin.app.App,
+  sslCreds: any
+): admin.app.App {
+  const initializeFirestoreSettings = (firestoreTarget: any, userSettings: any) => {
+    const isEnabled = isFeatureEnabled(frb, "admin_stubs");
+
+    if (!isEnabled) {
+      if (!hasInitializedFirestore) {
+        firestoreTarget.settings(userSettings);
+        hasInitializedFirestore = true;
+      }
+      return;
+    }
+
+    if (!hasInitializedFirestore && frb.ports.firestore) {
+      const emulatorSettings = {
+        projectId: frb.projectId,
+        port: frb.ports.firestore,
+        servicePath: "localhost",
+        service: "firestore.googleapis.com",
+        sslCreds,
+        customHeaders: {
+          Authorization: "Bearer owner",
+        },
+        ...userSettings,
+      };
+      firestoreTarget.settings(emulatorSettings);
+
+      new EmulatorLog("DEBUG", "set-firestore-settings", "", emulatorSettings).log();
+    } else if (!frb.ports.firestore && frb.triggerId) {
+      new EmulatorLog(
+        "WARN",
+        "runtime-status",
+        "The Cloud Firestore emulator is not running so database operations will fail with a " +
+          "'default credentials' error."
+      ).log();
+    }
+
+    hasInitializedFirestore = true;
+  };
+
+  const appProxy = new Proxied<admin.app.App>(original);
+  appProxy.when("firestore", (target: any) => {
+    // TODO: this 'typeof admin.firestore' should probably be 'Firestore' from @google-cloud/firestore
+    //       but I can't get all the type checking to play nice.
+    const firestoreProxy = new Proxied<typeof admin.firestore>(target.firestore);
+    return firestoreProxy
+      .applied(() => {
+        return new Proxied(target.firestore())
+          .when("settings", (firestoreTarget) => {
+            return (settings: any) => {
+              initializeFirestoreSettings(firestoreTarget, settings);
+            };
+          })
+          .any((firestoreTarget, field) => {
+            initializeFirestoreSettings(firestoreTarget, {});
+            return firestoreProxy.getOriginal(firestoreTarget, field);
+          })
+          .finalize();
+      })
+      .finalize();
+  });
+
+  return appProxy.finalize();
+}
+
 /*
   Here we set up some environment configs, but more importantly, we break GOOGLE_APPLICATION_CREDENTIALS
   and FIREBASE_CONFIG so that there's no way we (google-auth) can automatically auth. This is a safety
@@ -620,17 +672,17 @@ async function InitializeFunctionsConfigHelper(functionsDir: string): Promise<vo
   });
 
   const ff = require(functionsResolution);
-  new EmulatorLog("DEBUG", "runtime-status", "Checked functions.config()", {
+  logDebug("Checked functions.config()", {
     config: ff.config(),
-  }).log();
+  });
 
   const originalConfig = ff.config();
   const proxiedConfig = new Proxied(originalConfig)
     .any((parentConfig, parentKey) => {
-      new EmulatorLog("DEBUG", "runtime-status", "config() parent accessed!", {
+      logDebug("config() parent accessed!", {
         parentKey,
         parentConfig,
-      }).log();
+      });
 
       return new Proxied(parentConfig[parentKey] || ({} as { [key: string]: any }))
         .any((childConfig, childKey) => {
@@ -666,10 +718,10 @@ async function ProcessHTTPS(frb: FunctionsRuntimeBundle, trigger: EmulatedTrigge
   await new Promise((resolveEphemeralServer, rejectEphemeralServer) => {
     const handler = async (req: express.Request, res: express.Response) => {
       try {
-        new EmulatorLog("DEBUG", "runtime-status", `Ephemeral server used!`).log();
+        logDebug(`Ephemeral server used!`);
         const func = trigger.getRawFunction();
 
-        new EmulatorLog("DEBUG", "runtime-status", "Body" + (req as any).rawBody).log();
+        logDebug("Body: " + (req as any).rawBody);
         res.on("finish", () => {
           instance.close();
           resolveEphemeralServer();
@@ -728,11 +780,7 @@ async function ProcessBackground(
   new EmulatorLog("SYSTEM", "runtime-status", "ready").log();
 
   const proto = frb.proto;
-  new EmulatorLog(
-    "DEBUG",
-    "runtime-status",
-    `ProcessBackground: proto=${JSON.stringify(proto)}`
-  ).log();
+  logDebug("ProcessBackground", proto);
 
   // All formats of the payload should carry a "data" property. The "context" property does
   // not exist in all versions. Where it doesn't exist, context is everything besides data.
@@ -743,11 +791,7 @@ async function ProcessBackground(
   // This is due to the fact that the Firestore emulator sends payloads in a newer
   // format than production firestore.
   if (context.resource && context.resource.name) {
-    new EmulatorLog(
-      "DEBUG",
-      "runtime-status",
-      `ProcessBackground: lifting resource.name from resource ${JSON.stringify(context.resource)}`
-    ).log();
+    logDebug("ProcessBackground: lifting resource.name from resource", context.resource);
     context.resource = context.resource.name;
   }
 
@@ -773,14 +817,14 @@ async function Run(func: () => Promise<any>): Promise<any> {
 
   console.log = log;
 
-  new EmulatorLog("DEBUG", "runtime-status", `Ephemeral server survived.`).log();
+  logDebug(`Ephemeral server survived.`);
   if (caughtErr) {
     throw caughtErr;
   }
 }
 
 async function RunBackground(proto: any, func: CloudFunction<any>): Promise<any> {
-  new EmulatorLog("DEBUG", "runtime-status", `RunBackground: proto=${JSON.stringify(proto)}`).log();
+  logDebug("RunBackground", proto);
 
   await Run(() => {
     return func(proto.data, proto.context);
@@ -830,14 +874,18 @@ async function moduleResolutionDetective(frb: FunctionsRuntimeBundle, error: Err
   }).log();
 }
 
+function logDebug(msg: string, data?: any): void {
+  new EmulatorLog("DEBUG", "runtime-status", msg, data).log();
+}
+
 async function main(): Promise<void> {
   const serializedFunctionsRuntimeBundle = process.argv[2] || "{}";
   const serializedFunctionTrigger = process.argv[3];
 
-  new EmulatorLog("DEBUG", "runtime-status", "Functions runtime initialized.", {
+  logDebug("Functions runtime initialized.", {
     cwd: process.cwd(),
     node_version: process.versions.node,
-  }).log();
+  });
 
   const frb = JSON.parse(serializedFunctionsRuntimeBundle) as FunctionsRuntimeBundle;
 
@@ -847,11 +895,7 @@ async function main(): Promise<void> {
     }).log();
   }
 
-  new EmulatorLog(
-    "DEBUG",
-    "runtime-status",
-    `Disabled runtime features: ${JSON.stringify(frb.disabled_features)}`
-  ).log();
+  logDebug(`Disabled runtime features: ${JSON.stringify(frb.disabled_features)}`);
 
   const verified = await verifyDeveloperNodeModules(frb);
   if (!verified) {
@@ -901,10 +945,8 @@ async function main(): Promise<void> {
   new EmulatorLog("SYSTEM", "triggers-parsed", "", { triggers, triggerDefinitions }).log();
 
   if (!frb.triggerId) {
-    /*
-      This is a purely diagnostic call, it's used as a check to make sure developer code compiles and runs as
-      expected, so we don't have any function to invoke.
-     */
+    // This is a purely diagnostic call, it's used as a check to make sure developer code compiles and runs as
+    // expected, so we don't have any function to invoke.
     return;
   }
 
@@ -916,20 +958,16 @@ async function main(): Promise<void> {
     ).log();
     return;
   } else {
-    new EmulatorLog(
-      "DEBUG",
-      "runtime-status",
-      `Trigger "${frb.triggerId}" has been found, beginning invocation!`
-    ).log();
+    logDebug(`Trigger "${frb.triggerId}" has been found, beginning invocation!`);
   }
 
   const trigger = triggers[frb.triggerId];
-  new EmulatorLog("DEBUG", "runtime-status", "", trigger.definition).log();
+  logDebug("", trigger.definition);
   const mode = trigger.definition.httpsTrigger ? "HTTPS" : "BACKGROUND";
 
-  new EmulatorLog("DEBUG", "runtime-status", `Running ${frb.triggerId} in mode ${mode}`).log();
+  logDebug(`Running ${frb.triggerId} in mode ${mode}`);
 
-  if (!app) {
+  if (!defaultApp) {
     adminModuleProxy.initializeApp();
     new EmulatorLog("SYSTEM", "admin-auto-initialized", "").log();
   }


Test patch:
diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts
index 7aa3aace2..fbb0fe8ef 100644
--- a/src/test/emulators/functionsEmulatorRuntime.spec.ts
+++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts
@@ -187,6 +187,48 @@ describe("FunctionsEmulator-Runtime", () => {
         expect(logs["function-log"]).to.eq(1);
       }).timeout(TIMEOUT_MED);
 
+      it("should provide a stubbed Firestore through admin.firestore()", async () => {
+        const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => {
+          const admin = require("firebase-admin");
+          admin.initializeApp();
+          const firestore = admin.firestore();
+
+          return {
+            function_id: require("firebase-functions")
+              .firestore.document("test/test")
+              .onCreate(async () => {
+                // Need to use the Firestore object in order to trigger init.
+                const ref = firestore.collection("foo").doc("bar");
+                return true;
+              }),
+          };
+        });
+
+        const logs = await _countLogEntries(runtime);
+        expect(logs["set-firestore-settings"]).to.eq(1);
+      }).timeout(TIMEOUT_MED);
+
+      it("should provide a stubbed Firestore through app.firestore()", async () => {
+        const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => {
+          const admin = require("firebase-admin");
+          const app = admin.initializeApp();
+          const firestore = app.firestore();
+
+          return {
+            function_id: require("firebase-functions")
+              .firestore.document("test/test")
+              .onCreate(async () => {
+                // Need to use the Firestore object in order to trigger init.
+                const ref = firestore.collection("foo").doc("bar");
+                return true;
+              }),
+          };
+        });
+
+        const logs = await _countLogEntries(runtime);
+        expect(logs["set-firestore-settings"]).to.eq(1);
+      }).timeout(TIMEOUT_MED);
+
       it("should redirect Firestore write to emulator", async () => {
         const onRequestCopy = _.cloneDeep(
           FunctionRuntimeBundles.onRequest