Repository: firebase/firebase-tools (ts)

Issue:
Hosting emulator uses a different RTDB instance from Functions and Database emulators.
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->

<!--
Thank you for contributing to the Firebase community!

Think you found a bug?
=======================
Yeah, we're definitely not perfect! Please use this template and include a minimal repro when opening the issue. If you know how to solve the issue, please create a Pull Request, and we'd be happy to review it!

Have a feature request?
========================
Great, we love hearing how we can improve our products! However, do not use this template to submit a feature request. Please submit your feature requests to: https://firebase.google.com/support/contact/bugs-features/

Have a usage question?
=======================
We get lots of those and we love helping you, but GitHub is not the best place for them and they will be closed. Please take a look at the guide first: https://firebase.google.com/docs/cli/

If the official documentation doesn't help, try asking through our official support channel: https://firebase.google.com/support/

Additional locations to check for solutions or assistance from the community:
- Stack Overflow: https://stackoverflow.com/
- Firebase Slack Community: https://firebase.community/

*Please avoid duplicate posting across multiple channels!*
-->

### [REQUIRED] Environment info

<!-- What version of the Firebase CLI (`firebase-tools`) are you using? Note that your issue may already be fixed in the latest versions. The latest version can be found at https://github.com/firebase/firebase-tools/releases -->

<!-- Output of `firebase --version` -->

**firebase-tools:**
9.1.0

<!-- e.g. macOS, Windows, Ubuntu -->

**Platform:** Raspbian
Linux raspberrypi 5.4.79-v7l+

### [REQUIRED] Test case

<!-- Provide a minimal, complete, and verifiable example (http://stackoverflow.com/help/mcve) -->

functions/index.js

```js
const functions = require('firebase-functions');
const admin = require("firebase-admin");

admin.initializeApp();

exports.addMessage = functions.https.onRequest(async (req, res) => {
    const original = req.query.text;
    const messageRef = await admin.database().ref('/messages').push();
    await messageRef.set({original: original});
    res.json({result: `Message with ID: ${messageRef.key} added.`});
  });
```

public/index.html
[database-debug.log](https://github.com/firebase/firebase-tools/files/5740506/database-debug.log)
[firebase-debug.log](https://github.com/firebase/firebase-tools/files/5740507/firebase-debug.log)
[pubsub-debug.log](https://github.com/firebase/firebase-tools/files/5740508/pubsub-debug.log)
[ui-debug.log](https://github.com/firebase/firebase-tools/files/5740509/ui-debug.log)

```html
<html>
<head>
  <meta charset="utf-8">
  <title>DB Instance Name Bug</title>
  <script defer src="/__/firebase/8.2.1/firebase-app.js"></script>
  <script defer src="/__/firebase/8.2.1/firebase-database.js"></script>
  <script defer src="/__/firebase/init.js?useEmulator=true"></script>
</head>

<body>
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      firebase.database().ref('/messages').once('value').then(
        (snapshot) => console.log(snapshot.val()),
        (error) => console.log(error)
      );
    });
  </script>
</body>
</html>
```

### [REQUIRED] Steps to reproduce

<!-- Provide the steps needed to reproduce the issue with the above test case. -->

1. Create a new project in the Firebase console.
2. Make a new local directory for the project with `firebase init`.
   A. Select the project from (1). 
   B. Select database, hosting, emulators.
   C. Accept all defaults.
3. Create public/index.html and functions/index.js as above
4. Run `firebase emulators:start`
5. Invoke the addMessage function: curl http://localhost:5001/*project-id*/us-central1/addMessage?text=test
6. Load the hosted website: `http://localhost:5000/`

### [REQUIRED] Expected behavior

<!-- What is the expected behavior? -->

All three emulators, Functions, Database, and Hosting should all refer to the same instance.

When the page is loaded in (6), an object with the message from (5) should be logged to the console. 

I have verified that this project works as intended when deployed to production.

### [REQUIRED] Actual behavior

The Functions emulator and the Database emulator both use http://localhost:9000?ns=*project-id*, but the Hosting emulator connects to ws://localhost:9000/.ws?v=5&ns=*project-id*-default-rtdb 

The Hosting emulator uses a database with the "-default-rtdb" suffix, while the other two emulators do not use that suffix.

When the page is loaded in (6), a "null" is logged to the console.

After loading the page, the database emulator shows two databases, one with and one without the "-default-rtdb" suffix.

<!-- Run the command with --debug flag, and include the logs below. -->


Pull request description:
Fix incorrect databaseUrl in Functions emulator
<!--

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 #2965

Due to the new location support in Realtime Database it is no longer safe to assume that `https://<PROJECT-ID>.firebaseio.com` is the default RTDB instance location.  This PR uses the `getAdminSdkConfig` REST endpoint to fetch the real database URL and pass it down to the Functions emulator.

In order to make this behavior work offline there are two fallbacks:

  - Successful fetches populate a local cache which can be used when offline.
  - If the cache is empty we can try and guess the Admin SDK config using the old (and not so good) method.

This fallback behavior is based on what we do with web config in the Hosting emulator. 

### Scenarios Tested

See the setup in https://github.com/firebase/firebase-tools/issues/2965.  I created a brand new project and created the defalt RTDB through `firebase init`.

First add a message using the Cloud Function:
```
$ http http://localhost:5001/fir-tools-2965/us-central1/addMessage?text="hello"
HTTP/1.1 200 OK
connection: keep-alive
content-length: 57
content-type: application/json; charset=utf-8
date: Mon, 28 Dec 2020 18:14:23 GMT
etag: W/"39-bGCJNRIDTzsIri/GyVcXPXLVrbs"
x-powered-by: Express

{
    "result": "Message with ID: -MPeYivLKiIxoGNp2Eil added."
}
```

I can see that the data goes to the right place in the Emulator UI:
![Screen Shot 2020-12-28 at 6 15 19 PM](https://user-images.githubusercontent.com/8466666/103234890-c77a8f80-490e-11eb-8773-0271cf74bfe3.png)

Finally if I load the hosting emulator I can see the correct logs:
![Screen Shot 2020-12-28 at 6 16 21 PM](https://user-images.githubusercontent.com/8466666/103234953-e4af5e00-490e-11eb-83d2-0eb48682dd30.png)


### Sample Commands

N/A

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a510167cd..083919035 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,1 +1,2 @@
 - Allow more than 100 concurrent connections to the Realtime Database emulator.
+- Fixes incorrect `databaseURL` inside the Cloud Functions emulator for new projects (#2965).
diff --git a/src/emulator/adminSdkConfig.ts b/src/emulator/adminSdkConfig.ts
new file mode 100644
index 000000000..aa605f453
--- /dev/null
+++ b/src/emulator/adminSdkConfig.ts
@@ -0,0 +1,77 @@
+import { firebaseApiOrigin } from "../api";
+import * as apiv2 from "../apiv2";
+import { configstore } from "../configstore";
+import { FirebaseError } from "../error";
+import * as logger from "../logger";
+
+export type AdminSdkConfig = {
+  projectId: string;
+  databaseURL?: string;
+  storageBucket?: string;
+  locationId?: string;
+};
+
+const _CONFIGSTORE_KEY = "adminsdkconfig";
+
+/**
+ * When all else fails we can "guess" the AdminSdkConfig, although this is likely to
+ * be incorrect.
+ */
+export function constructDefaultAdminSdkConfig(projectId: string): AdminSdkConfig {
+  // Do our best to provide reasonable FIREBASE_CONFIG, based on firebase-functions implementation
+  // https://github.com/firebase/firebase-functions/blob/59d6a7e056a7244e700dc7b6a180e25b38b647fd/src/setup.ts#L45
+  return {
+    projectId: projectId,
+    databaseURL: process.env.DATABASE_URL || `https://${projectId}.firebaseio.com`,
+    storageBucket: process.env.STORAGE_BUCKET_URL || `${projectId}.appspot.com`,
+  };
+}
+
+/**
+ * Get the Admin SDK configuration associated with a project, falling back to a cache when offline.
+ */
+export async function getProjectAdminSdkConfigOrCached(
+  projectId: string
+): Promise<AdminSdkConfig | undefined> {
+  try {
+    const config = await getProjectAdminSdkConfig(projectId);
+    setCacheAdminSdkConfig(projectId, config);
+    return config;
+  } catch (e) {
+    logger.debug(`Failed to get Admin SDK config for ${projectId}, falling back to cache`, e);
+    return getCachedAdminSdkConfig(projectId);
+  }
+}
+
+/**
+ * Gets the Admin SDK configuration associated with a project.
+ */
+export async function getProjectAdminSdkConfig(projectId: string): Promise<AdminSdkConfig> {
+  const apiClient = new apiv2.Client({
+    auth: true,
+    apiVersion: "v1beta1",
+    urlPrefix: firebaseApiOrigin,
+  });
+
+  try {
+    const res = await apiClient.get<AdminSdkConfig>(`projects/${projectId}/adminSdkConfig`);
+    return res.body;
+  } catch (err) {
+    throw new FirebaseError(
+      `Failed to get Admin SDK for Firebase project ${projectId}. ` +
+        "Please make sure the project exists and your account has permission to access it.",
+      { exit: 2, original: err }
+    );
+  }
+}
+
+function setCacheAdminSdkConfig(projectId: string, config: AdminSdkConfig) {
+  const allConfigs = configstore.get(_CONFIGSTORE_KEY) || {};
+  allConfigs[projectId] = config;
+  configstore.set(_CONFIGSTORE_KEY, allConfigs);
+}
+
+function getCachedAdminSdkConfig(projectId: string): AdminSdkConfig | undefined {
+  const allConfigs = configstore.get(_CONFIGSTORE_KEY) || {};
+  return allConfigs[projectId];
+}
diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts
index d0a35c24b..310398759 100644
--- a/src/emulator/functionsEmulator.ts
+++ b/src/emulator/functionsEmulator.ts
@@ -42,6 +42,11 @@ import { FirebaseError } from "../error";
 import { WorkQueue } from "./workQueue";
 import { createDestroyer } from "../utils";
 import { getCredentialPathAsync } from "../defaultCredentials";
+import {
+  getProjectAdminSdkConfigOrCached,
+  AdminSdkConfig,
+  constructDefaultAdminSdkConfig,
+} from "./adminSdkConfig";
 
 const EVENT_INVOKE = "functions:invoke";
 
@@ -128,9 +133,10 @@ export class FunctionsEmulator implements EmulatorInstance {
   private workerPool: RuntimeWorkerPool;
   private workQueue: WorkQueue;
   private logger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS);
-
   private multicastTriggers: { [s: string]: string[] } = {};
 
+  private adminSdkConfig: AdminSdkConfig;
+
   constructor(private args: FunctionsEmulatorArgs) {
     // TODO: Would prefer not to have static state but here we are!
     EmulatorLogger.verbosity = this.args.quiet ? Verbosity.QUIET : Verbosity.DEBUG;
@@ -141,6 +147,10 @@ export class FunctionsEmulator implements EmulatorInstance {
       this.args.disabledRuntimeFeatures.timeout = true;
     }
 
+    this.adminSdkConfig = {
+      projectId: this.args.projectId,
+    };
+
     const mode = this.args.debugPort
       ? FunctionsExecutionMode.SEQUENTIAL
       : FunctionsExecutionMode.AUTO;
@@ -317,6 +327,18 @@ export class FunctionsEmulator implements EmulatorInstance {
       ...this.args.env,
     };
 
+    const adminSdkConfig = await getProjectAdminSdkConfigOrCached(this.args.projectId);
+    if (adminSdkConfig) {
+      this.adminSdkConfig = adminSdkConfig;
+    } else {
+      this.logger.logLabeled(
+        "WARN",
+        "functions",
+        "Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect."
+      );
+      this.adminSdkConfig = constructDefaultAdminSdkConfig(this.args.projectId);
+    }
+
     const { host, port } = this.getInfo();
     this.workQueue.start();
     const server = this.createHubServer().listen(port, host);
@@ -685,6 +707,10 @@ export class FunctionsEmulator implements EmulatorInstance {
         pubsub: EmulatorRegistry.getInfo(Emulators.PUBSUB),
         auth: EmulatorRegistry.getInfo(Emulators.AUTH),
       },
+      adminSdkConfig: {
+        databaseURL: this.adminSdkConfig.databaseURL,
+        storageBucket: this.adminSdkConfig.storageBucket,
+      },
       disabled_features: this.args.disabledRuntimeFeatures,
     };
   }
diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts
index 140ca3860..43e673b73 100644
--- a/src/emulator/functionsEmulatorRuntime.ts
+++ b/src/emulator/functionsEmulatorRuntime.ts
@@ -654,20 +654,22 @@ async function initializeEnvironmentalVariables(frb: FunctionsRuntimeBundle): Pr
   const functionsGt380 = compareVersionStrings(functionsResolution.version, "3.8.0") >= 0;
   let emulatedDatabaseURL = undefined;
   if (frb.emulators.database && functionsGt380) {
-    emulatedDatabaseURL = `http://${formatHost(frb.emulators.database)}?ns=${
-      process.env.GCLOUD_PROJECT
-    }`;
+    // Database URL will look like one of:
+    //  - https://${namespace}.firebaseio.com
+    //  - https://${namespace}.${location}.firebasedatabase.app
+    let ns = frb.projectId;
+    if (frb.adminSdkConfig.databaseURL) {
+      const asUrl = new URL(frb.adminSdkConfig.databaseURL);
+      ns = asUrl.hostname.split(".")[0];
+    }
+
+    emulatedDatabaseURL = `http://${formatHost(frb.emulators.database)}/?ns=${ns}`;
   }
 
-  // Do our best to provide reasonable FIREBASE_CONFIG, based on firebase-functions implementation
-  // https://github.com/firebase/firebase-functions/blob/59d6a7e056a7244e700dc7b6a180e25b38b647fd/src/setup.ts#L45
   process.env.FIREBASE_CONFIG = JSON.stringify({
-    databaseURL:
-      process.env.DATABASE_URL ||
-      emulatedDatabaseURL ||
-      `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`,
-    storageBucket: process.env.STORAGE_BUCKET_URL || `${process.env.GCLOUD_PROJECT}.appspot.com`,
-    projectId: process.env.GCLOUD_PROJECT,
+    storageBucket: frb.adminSdkConfig.storageBucket,
+    databaseURL: emulatedDatabaseURL || frb.adminSdkConfig.databaseURL,
+    projectId: frb.projectId,
   });
 
   if (frb.triggerId) {
diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts
index 63eefc3af..6ddb8191c 100644
--- a/src/emulator/functionsEmulatorShared.ts
+++ b/src/emulator/functionsEmulatorShared.ts
@@ -66,6 +66,10 @@ export interface FunctionsRuntimeBundle {
       port: number;
     };
   };
+  adminSdkConfig: {
+    databaseURL?: string;
+    storageBucket?: string;
+  };
   socketPath?: string;
   disabled_features?: FunctionsRuntimeFeatures;
   nodeMajorVersion?: number;


Test patch:
diff --git a/src/test/emulators/fixtures.ts b/src/test/emulators/fixtures.ts
index 0f873ee82..3e3b1eda5 100644
--- a/src/test/emulators/fixtures.ts
+++ b/src/test/emulators/fixtures.ts
@@ -4,8 +4,12 @@ export const TIMEOUT_LONG = 10000;
 export const TIMEOUT_MED = 5000;
 
 export const MODULE_ROOT = findModuleRoot("firebase-tools", __dirname);
-export const FunctionRuntimeBundles = {
+export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = {
   onCreate: {
+    adminSdkConfig: {
+      databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
+      storageBucket: "fake-project-id.appspot.com",
+    },
     emulators: {
       firestore: {
         host: "localhost",
@@ -39,9 +43,12 @@ export const FunctionRuntimeBundles = {
     },
     triggerId: "function_id",
     projectId: "fake-project-id",
-  } as FunctionsRuntimeBundle,
-
+  },
   onWrite: {
+    adminSdkConfig: {
+      databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
+      storageBucket: "fake-project-id.appspot.com",
+    },
     emulators: {
       firestore: {
         host: "localhost",
@@ -75,9 +82,12 @@ export const FunctionRuntimeBundles = {
     },
     triggerId: "function_id",
     projectId: "fake-project-id",
-  } as FunctionsRuntimeBundle,
-
+  },
   onDelete: {
+    adminSdkConfig: {
+      databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
+      storageBucket: "fake-project-id.appspot.com",
+    },
     emulators: {
       firestore: {
         host: "localhost",
@@ -111,9 +121,12 @@ export const FunctionRuntimeBundles = {
     },
     triggerId: "function_id",
     projectId: "fake-project-id",
-  } as FunctionsRuntimeBundle,
-
+  },
   onUpdate: {
+    adminSdkConfig: {
+      databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
+      storageBucket: "fake-project-id.appspot.com",
+    },
     emulators: {
       firestore: {
         host: "localhost",
@@ -159,9 +172,12 @@ export const FunctionRuntimeBundles = {
     },
     triggerId: "function_id",
     projectId: "fake-project-id",
-  } as FunctionsRuntimeBundle,
-
+  },
   onRequest: {
+    adminSdkConfig: {
+      databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
+      storageBucket: "fake-project-id.appspot.com",
+    },
     emulators: {
       firestore: {
         host: "localhost",
@@ -171,5 +187,5 @@ export const FunctionRuntimeBundles = {
     cwd: MODULE_ROOT,
     triggerId: "function_id",
     projectId: "fake-project-id",
-  } as FunctionsRuntimeBundle,
+  },
 };
diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts
index d8f3e2c8f..8f6752d21 100644
--- a/src/test/emulators/functionsEmulatorRuntime.spec.ts
+++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts
@@ -236,7 +236,7 @@ describe("FunctionsEmulator-Runtime", () => {
       }).timeout(TIMEOUT_MED);
 
       it("should expose Firestore prod when the emulator is not running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         frb.emulators = {};
 
         const worker = invokeRuntimeWithFunctions(frb, () => {
@@ -260,7 +260,7 @@ describe("FunctionsEmulator-Runtime", () => {
       }).timeout(TIMEOUT_MED);
 
       it("should set FIRESTORE_EMULATOR_HOST when the emulator is running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         frb.emulators = {
           firestore: {
             host: "localhost",
@@ -286,7 +286,7 @@ describe("FunctionsEmulator-Runtime", () => {
       }).timeout(TIMEOUT_MED);
 
       it("should expose a stubbed Firestore when the emulator is running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         frb.emulators = {
           firestore: {
             host: "localhost",
@@ -315,7 +315,7 @@ describe("FunctionsEmulator-Runtime", () => {
       }).timeout(TIMEOUT_MED);
 
       it("should expose RTDB prod when the emulator is not running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         frb.emulators = {};
 
         const worker = invokeRuntimeWithFunctions(frb, () => {
@@ -337,11 +337,11 @@ describe("FunctionsEmulator-Runtime", () => {
 
         const data = await callHTTPSFunction(worker, frb);
         const info = JSON.parse(data);
-        expect(info.url).to.eql("https://fake-project-id.firebaseio.com/");
+        expect(info.url).to.eql("https://fake-project-id-default-rtdb.firebaseio.com/");
       }).timeout(TIMEOUT_MED);
 
       it("should set FIREBASE_DATABASE_EMULATOR_HOST when the emulator is running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         frb.emulators = {
           database: {
             host: "localhost",
@@ -366,7 +366,7 @@ describe("FunctionsEmulator-Runtime", () => {
       }).timeout(TIMEOUT_MED);
 
       it("should expose a stubbed RTDB when the emulator is running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         frb.emulators = {
           database: {
             host: "localhost",
@@ -396,7 +396,7 @@ describe("FunctionsEmulator-Runtime", () => {
       }).timeout(TIMEOUT_MED);
 
       it("should return an emulated databaseURL when RTDB emulator is running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         frb.emulators = {
           database: {
             host: "localhost",
@@ -417,11 +417,11 @@ describe("FunctionsEmulator-Runtime", () => {
 
         const data = await callHTTPSFunction(worker, frb);
         const info = JSON.parse(data);
-        expect(info.databaseURL).to.eql(`http://localhost:9090?ns=${frb.projectId}`);
+        expect(info.databaseURL).to.eql(`http://localhost:9090/?ns=fake-project-id-default-rtdb`);
       }).timeout(TIMEOUT_MED);
 
       it("should return a real databaseURL when RTDB emulator is not running", async () => {
-        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+        const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
         const worker = invokeRuntimeWithFunctions(frb, () => {
           const admin = require("firebase-admin");
           admin.initializeApp();
@@ -435,12 +435,12 @@ describe("FunctionsEmulator-Runtime", () => {
 
         const data = await callHTTPSFunction(worker, frb);
         const info = JSON.parse(data);
-        expect(info.databaseURL).to.eql(`https://${frb.projectId}.firebaseio.com`);
+        expect(info.databaseURL).to.eql(frb.adminSdkConfig.databaseURL!);
       }).timeout(TIMEOUT_MED);
     });
 
     it("should set FIREBASE_AUTH_EMULATOR_HOST when the emulator is running", async () => {
-      const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest) as FunctionsRuntimeBundle;
+      const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest);
       frb.emulators = {
         auth: {
           host: "localhost",
diff --git a/src/test/emulators/functionsRuntimeWorker.spec.ts b/src/test/emulators/functionsRuntimeWorker.spec.ts
index 70667ac45..462a5bb12 100644
--- a/src/test/emulators/functionsRuntimeWorker.spec.ts
+++ b/src/test/emulators/functionsRuntimeWorker.spec.ts
@@ -94,6 +94,11 @@ class MockRuntimeBundle implements FunctionsRuntimeBundle {
   triggerType = EmulatedTriggerType.HTTPS;
   cwd = "/home/users/dir";
   emulators = {};
+  adminSdkConfig = {
+    projectId: "project-1234",
+    datbaseURL: "https://project-1234-default-rtdb.firebaseio.com",
+    storageBucket: "project-1234.appspot.com",
+  };
 
   constructor(public triggerId: string) {}
 }