Repository: firebase/firebase-tools (ts)

Issue:
default PARAM isn't being injected during extension emulation
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->

### [REQUIRED] Environment info

**firebase-tools:**
8.17.0

**Platform:**
macOS Catalina 10.15.4

### [REQUIRED] Test case

See steps to reproduce.

### [REQUIRED] Steps to reproduce

1. git clone `git@github.com:firebase/extensions.git`
2. run `npm i` from root of extension's directory.
3. cd `delete-user-data` directory
4. create `test-params.env` file. Add environment variables & insert project id as appropriate:
```env
LOCATION=europe-west2
STORAGE_PATHS={DEFAULT}/test/{UID}-test.png
SELECTED_DATABASE_INSTANCE=[INSERT YOUR OWN PROJECT ID]
FIRESTORE_DELETE_MODE=shallow
```
5. run CLI command with your project id: `firebase ext:dev:emulators:start --test-params=test-params.env --project=[INSERT YOUR OWN PROJECT ID]` 

### [REQUIRED] Expected behavior

The auth emulator and the cloud function emulator running.

### [REQUIRED] Actual behavior

The emulator command is erroring because the `${DATABASE_INSTANCE}'` param which is auto populated in the `extensions.yaml` during build time, is not being recognised by the emulator. 
```
Error: The extension.yaml has the following errors: 
  - Param SELECTED_DATABASE_INSTANCE has default value '${DATABASE_INSTANCE}', which does not pass the validationRegex ^([0-9a-z_.-]*)$
```

I did also try `${param:DATABASE_INSTANCE}` in the `extension.yaml` but I got the same error:

```
Error: The extension.yaml has the following errors: 
  - Param SELECTED_DATABASE_INSTANCE has default value '${param:DATABASE_INSTANCE}', which does not pass the validationRegex ^([0-9a-z_.-]*)$
```

Pull request description:
Fixes a number of issues where ext:dev:emulator:* did not allow valid extension configurations
### Description
Fixes a bunch of issues where the param and spec validation for the extension emulator behaved badly:
- False-y default values were broken in multiple places. I went through and fixed these.
- Param validation was happening before variable substitution. I refactored buildOptions to make sure this happened first.
- Spec validation for params with defaults did not substitute autopopulated params into those defaults. We were also checking defaults against validationRegex. This meant that these regex checks would fail for some defaults that referenced autopopulated params. I removed this.

I also did some minor modernization and cleanup of these files.
Fixes #2928 
- ### Scenarios Tested
I tested a number of extension that Invertase reported were broken on the emulator. I also found some cases where the Storage and auth emulators weren't working correctly with extension - I'm going to try to fix that in a followup.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52aefbb25..84f5cbfb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Fixes a number of issues where `ext:dev:emulator:*` was not providing the correct parameter values to the emulated extension (#2928)
 - Support loading Firebase Functions packaged as an ES module. (#3485)
 - Fixes Cloud Storage emulator missing classes. (#3541)
 - Add missing properties to Authentication functions `context` in the Functions emulator. (#3536)
diff --git a/src/extensions/askUserForParam.ts b/src/extensions/askUserForParam.ts
index 57ad0ba97..49556a8eb 100644
--- a/src/extensions/askUserForParam.ts
+++ b/src/extensions/askUserForParam.ts
@@ -13,7 +13,7 @@ export function checkResponse(response: string, spec: Param): boolean {
   let valid = true;
   let responses: string[];
 
-  if (spec.required && !response) {
+  if (spec.required && (response == "" || response == undefined)) {
     utils.logWarning(`Param ${spec.param} is required, but no value was provided.`);
     return false;
   }
@@ -139,7 +139,7 @@ export async function ask(
   }
 
   utils.logLabeledBullet(logPrefix, "answer the questions below to configure your extension:");
-  const substituted = substituteParams(paramSpecs, firebaseProjectParams);
+  const substituted = substituteParams<Param[]>(paramSpecs, firebaseProjectParams);
   const result: any = {};
   const promises = _.map(substituted, (paramSpec: Param) => {
     return async () => {
diff --git a/src/extensions/emulator/optionsHelper.ts b/src/extensions/emulator/optionsHelper.ts
index 37e587599..2a1f4a51d 100644
--- a/src/extensions/emulator/optionsHelper.ts
+++ b/src/extensions/emulator/optionsHelper.ts
@@ -6,7 +6,7 @@ import * as paramHelper from "../paramHelper";
 import * as specHelper from "./specHelper";
 import * as localHelper from "../localHelper";
 import * as triggerHelper from "./triggerHelper";
-import { Resource } from "../extensionsApi";
+import { ExtensionSpec, Resource } from "../extensionsApi";
 import * as extensionsHelper from "../extensionsHelper";
 import { Config } from "../../config";
 import { FirebaseError } from "../../error";
@@ -17,18 +17,15 @@ import { Emulators } from "../../emulator/types";
 export async function buildOptions(options: any): Promise<any> {
   const extensionDir = localHelper.findExtensionYaml(process.cwd());
   options.extensionDir = extensionDir;
-  const extensionYaml = await specHelper.readExtensionYaml(extensionDir);
-  extensionsHelper.validateSpec(extensionYaml);
-
-  const params = await paramHelper.readParamsFile(options.testParams);
-  extensionsHelper.validateCommandLineParams(params, extensionYaml.params || []);
-  params["PROJECT_ID"] = getProjectId(options, false);
-  params["EXT_INSTANCE_ID"] = params["EXT_INSTANCE_ID"] || extensionYaml.name;
-  params["DATABASE_INSTANCE"] = params["PROJECT_ID"];
-  params["DATABASE_URL"] = `https://${params["DATABASE_INSTANCE"]}.firebaseio.com`;
-  params["STORAGE_BUCKET"] = `${params["PROJECT_ID"]}.appspot.com`;
+  const spec = await specHelper.readExtensionYaml(extensionDir);
+  extensionsHelper.validateSpec(spec);
+
+  const params = await getParams(options, spec);
+
+  extensionsHelper.validateCommandLineParams(params, spec.params);
+
   const functionResources = specHelper.getFunctionResourcesWithParamSubstitution(
-    extensionYaml,
+    spec,
     params
   ) as Resource[];
   let testConfig;
@@ -46,6 +43,27 @@ export async function buildOptions(options: any): Promise<any> {
   return options;
 }
 
+// Exported for testing
+export async function getParams(options: any, extensionSpec: ExtensionSpec) {
+  const projectId = getProjectId(options, false);
+  const userParams = await paramHelper.readParamsFile(options.testParams);
+  const autoParams = {
+    PROJECT_ID: projectId,
+    EXT_INSTANCE_ID: extensionSpec.name,
+    DATABASE_INSTANCE: projectId,
+    DATABASE_URL: `https://${projectId}.firebaseio.com`,
+    STORAGE_BUCKET: `${projectId}.appspot.com`,
+  };
+  const unsubbedParamsWithoutDefaults = Object.assign(autoParams, userParams);
+
+  const unsubbedParams = extensionsHelper.populateDefaultParams(
+    unsubbedParamsWithoutDefaults,
+    extensionSpec.params
+  );
+  // Run a substitution to support params that reference other params.
+  return extensionsHelper.substituteParams<Record<string, string>>(unsubbedParams, unsubbedParams);
+}
+
 /**
  * Checks and warns if the test config is missing fields
  * that are relevant for the extension being emulated.
diff --git a/src/extensions/emulator/specHelper.ts b/src/extensions/emulator/specHelper.ts
index b6f90ece8..ab6f84a36 100644
--- a/src/extensions/emulator/specHelper.ts
+++ b/src/extensions/emulator/specHelper.ts
@@ -77,7 +77,7 @@ export function getFunctionResourcesWithParamSubstitution(
   const rawResources = extensionSpec.resources.filter((resource) =>
     validFunctionTypes.includes(resource.type)
   );
-  return substituteParams(rawResources, params);
+  return substituteParams<Resource[]>(rawResources, params);
 }
 
 export function getFunctionProperties(resources: Resource[]) {
diff --git a/src/extensions/extensionsApi.ts b/src/extensions/extensionsApi.ts
index ddd5bb762..89869a9d2 100644
--- a/src/extensions/extensionsApi.ts
+++ b/src/extensions/extensionsApi.ts
@@ -102,7 +102,7 @@ export interface ExtensionSpec {
   license?: string;
   releaseNotesUrl?: string;
   sourceUrl: string;
-  params?: Param[];
+  params: Param[];
   preinstallContent?: string;
   postinstallContent?: string;
   readmeContent?: string;
diff --git a/src/extensions/extensionsHelper.ts b/src/extensions/extensionsHelper.ts
index 37fc50b8b..d8ed9ae45 100644
--- a/src/extensions/extensionsHelper.ts
+++ b/src/extensions/extensionsHelper.ts
@@ -2,7 +2,6 @@ import * as _ from "lodash";
 import * as clc from "cli-color";
 import * as ora from "ora";
 import * as semver from "semver";
-import * as fs from "fs";
 import * as marked from "marked";
 
 import { storageOrigin } from "../api";
@@ -54,13 +53,20 @@ export enum SourceOrigin {
 }
 
 export const logPrefix = "extensions";
-export const validLicenses = ["apache-2.0"];
+const VALID_LICENSES = ["apache-2.0"];
 // Extension archive URLs must be HTTPS.
-export const urlRegex = /^https:/;
+export const URL_REGEX = /^https:/;
 export const EXTENSIONS_BUCKET_NAME = envOverride(
   "FIREBASE_EXTENSIONS_UPLOAD_BUCKET",
   "firebase-ext-eap-uploads"
 );
+const AUTOPOPULATED_PARAM_NAMES = [
+  "PROJECT_ID",
+  "STORAGE_BUCKET",
+  "EXT_INSTANCE_ID",
+  "DATABASE_INSTANCE",
+  "DATABASE_URL",
+];
 // Placeholders that can be used whever param substitution is needed, but are not available.
 export const AUTOPOULATED_PARAM_PLACEHOLDERS = {
   PROJECT_ID: "project-id",
@@ -69,7 +75,7 @@ export const AUTOPOULATED_PARAM_PLACEHOLDERS = {
   DATABASE_INSTANCE: "project-id-default-rtdb",
   DATABASE_URL: "https://project-id-default-rtdb.firebaseio.com",
 };
-export const resourceTypeToNiceName: { [key: string]: string } = {
+export const resourceTypeToNiceName: Record<string, string> = {
   "firebaseextensions.v1beta.function": "Cloud Function",
 };
 
@@ -117,7 +123,7 @@ export async function getFirebaseProjectParams(projectId: string): Promise<any>
  * @param params params to substitute the placeholders for
  * @return Resources object with substituted params
  */
-export function substituteParams(original: object[], params: { [key: string]: string }): Param[] {
+export function substituteParams<T>(original: T, params: Record<string, string>): T {
   const startingString = JSON.stringify(original);
   const applySubstitution = (str: string, paramVal: string, paramKey: string): string => {
     const exp1 = new RegExp("\\$\\{" + paramKey + "\\}", "g");
@@ -138,21 +144,21 @@ export function substituteParams(original: object[], params: { [key: string]: st
  * @param paramSpec information on params parsed from extension.yaml
  * @return JSON object of params
  */
-export function populateDefaultParams(paramVars: any, paramSpec: any): any {
+export function populateDefaultParams(paramVars: Record<string, string>, paramSpecs: Param[]): any {
   const newParams = paramVars;
 
-  _.forEach(paramSpec, (env) => {
-    if (!paramVars[env.param]) {
-      if (env.default) {
-        newParams[env.param] = env.default;
-      } else if (env.required) {
+  for (const param of paramSpecs) {
+    if (!paramVars[param.param]) {
+      if (param.default != undefined) {
+        newParams[param.param] = param.default;
+      } else if (param.required) {
         throw new FirebaseError(
-          `${env.param} has not been set in the given params file` +
+          `${param.param} has not been set in the given params file` +
             " and there is no default available. Please set this variable before installing again."
         );
       }
     }
-  });
+  }
 
   return newParams;
 }
@@ -163,28 +169,26 @@ export function populateDefaultParams(paramVars: any, paramSpec: any): any {
  * @param paramSpec information on params parsed from extension.yaml
  */
 export function validateCommandLineParams(
-  envVars: { [key: string]: string },
-  paramSpec: any[]
+  envVars: Record<string, string>,
+  paramSpec: Param[]
 ): void {
-  if (_.size(envVars) > _.size(paramSpec)) {
-    const paramList = _.map(paramSpec, (param) => {
-      return param.param;
-    });
-    const misnamedParams = Object.keys(envVars).filter((key: any) => {
-      return !paramList.includes(key);
-    });
-    logger.info(
+  const paramNames = paramSpec.map((p) => p.param);
+  const misnamedParams = Object.keys(envVars).filter((key: string) => {
+    return !paramNames.includes(key) && !AUTOPOPULATED_PARAM_NAMES.includes(key);
+  });
+  if (misnamedParams.length) {
+    logger.warn(
       "Warning: The following params were specified in your env file but do not exist in the extension spec: " +
         `${misnamedParams.join(", ")}.`
     );
   }
   let allParamsValid = true;
-  _.forEach(paramSpec, (param) => {
+  for (const param of paramSpec) {
     // Warns if invalid response was found in environment file.
     if (!checkResponse(envVars[param.param], param)) {
       allParamsValid = false;
     }
-  });
+  }
   if (!allParamsValid) {
     throw new FirebaseError(`Some param values are not valid. Please check your params file.`);
   }
@@ -210,9 +214,9 @@ export function validateSpec(spec: any) {
     errors.push("extension.yaml is missing required field: license");
   } else {
     const formattedLicense = String(spec.license).toLocaleLowerCase();
-    if (!validLicenses.includes(formattedLicense)) {
+    if (!VALID_LICENSES.includes(formattedLicense)) {
       errors.push(
-        `license field in extension.yaml is invalid. Valid value(s): ${validLicenses.join(", ")}`
+        `license field in extension.yaml is invalid. Valid value(s): ${VALID_LICENSES.join(", ")}`
       );
     }
   }
@@ -263,17 +267,6 @@ export function validateSpec(spec: any) {
           } cannot have options because it is type STRING`
         );
       }
-      if (
-        param.default &&
-        param.validationRegex &&
-        !RegExp(param.validationRegex).test(param.default)
-      ) {
-        errors.push(
-          `Param${param.param ? ` ${param.param}` : ""} has default value '${
-            param.default
-          }', which does not pass the validationRegex ${param.validationRegex}`
-        );
-      }
     }
     if (
       param.type &&
@@ -394,7 +387,10 @@ export async function publishExtensionVersionFromLocalSource(
 
   // Substitute deepcopied spec with autopopulated params, and make sure that it passes basic extension.yaml validation.
   const subbedSpec = JSON.parse(JSON.stringify(extensionSpec));
-  subbedSpec.params = substituteParams(extensionSpec.params || [], AUTOPOULATED_PARAM_PLACEHOLDERS);
+  subbedSpec.params = substituteParams<Param[]>(
+    extensionSpec.params || [],
+    AUTOPOULATED_PARAM_PLACEHOLDERS
+  );
   validateSpec(subbedSpec);
 
   const consent = await confirmExtensionVersion(publisherId, extensionId, extensionSpec.version);
@@ -490,7 +486,7 @@ export async function createSourceFromLocation(
   let packageUri: string;
   let extensionRoot: string;
   let objectPath = "";
-  if (!urlRegex.test(sourceUri)) {
+  if (!URL_REGEX.test(sourceUri)) {
     const uploadSpinner = ora.default(" Archiving and uploading extension source code");
     try {
       uploadSpinner.start();
@@ -635,7 +631,7 @@ export async function instanceIdExists(projectId: string, instanceId: string): P
 }
 
 export function isUrlPath(extInstallPath: string): boolean {
-  return urlRegex.test(extInstallPath);
+  return URL_REGEX.test(extInstallPath);
 }
 
 export function isLocalPath(extInstallPath: string): boolean {
diff --git a/src/extensions/paramHelper.ts b/src/extensions/paramHelper.ts
index f63ab3d8b..da4d4dea4 100644
--- a/src/extensions/paramHelper.ts
+++ b/src/extensions/paramHelper.ts
@@ -105,10 +105,16 @@ export async function promptForNewParams(
     return param1.type === param2.type && param1.param === param2.param;
   };
   let paramsDiffDeletions = _.differenceWith(spec.params, _.get(newSpec, "params", []), comparer);
-  paramsDiffDeletions = substituteParams(paramsDiffDeletions, firebaseProjectParams);
+  paramsDiffDeletions = substituteParams<extensionsApi.Param[]>(
+    paramsDiffDeletions,
+    firebaseProjectParams
+  );
 
   let paramsDiffAdditions = _.differenceWith(newSpec.params, _.get(spec, "params", []), comparer);
-  paramsDiffAdditions = substituteParams(paramsDiffAdditions, firebaseProjectParams);
+  paramsDiffAdditions = substituteParams<extensionsApi.Param[]>(
+    paramsDiffAdditions,
+    firebaseProjectParams
+  );
 
   if (paramsDiffDeletions.length) {
     logger.info("The following params will no longer be used:");
diff --git a/src/extensions/updateHelper.ts b/src/extensions/updateHelper.ts
index 4fad0bf50..e9bb577c2 100644
--- a/src/extensions/updateHelper.ts
+++ b/src/extensions/updateHelper.ts
@@ -11,7 +11,7 @@ import {
   createSourceFromLocation,
   logPrefix,
   SourceOrigin,
-  urlRegex,
+  URL_REGEX,
   isLocalOrURLPath,
 } from "./extensionsHelper";
 import * as utils from "../utils";


Test patch:
diff --git a/src/test/extensions/emulator/optionsHelper.spec.ts b/src/test/extensions/emulator/optionsHelper.spec.ts
new file mode 100644
index 000000000..4147ac1b7
--- /dev/null
+++ b/src/test/extensions/emulator/optionsHelper.spec.ts
@@ -0,0 +1,119 @@
+import { expect } from "chai";
+import * as sinon from "sinon";
+
+import * as optionsHelper from "../../../extensions/emulator/optionsHelper";
+import { ExtensionSpec } from "../../../extensions/extensionsApi";
+import * as paramHelper from "../../../extensions/paramHelper";
+
+describe("optionsHelper", () => {
+  describe("getParams", () => {
+    const testOptions = {
+      project: "test",
+      testParams: "test.env",
+    };
+    const autoParams = {
+      PROJECT_ID: "test",
+      EXT_INSTANCE_ID: "test",
+      DATABASE_INSTANCE: "test",
+      DATABASE_URL: "https://test.firebaseio.com",
+      STORAGE_BUCKET: "test.appspot.com",
+    };
+    let testSpec: ExtensionSpec;
+    let readParamsFileStub: sinon.SinonStub;
+
+    beforeEach(() => {
+      testSpec = {
+        name: "test",
+        version: "0.1.0",
+        resources: [],
+        sourceUrl: "https://my.stuff.com",
+        params: [],
+      };
+      readParamsFileStub = sinon.stub(paramHelper, "readParamsFile");
+    });
+
+    afterEach(() => {
+      readParamsFileStub.restore();
+    });
+
+    it("should return user and autopopulated params", () => {
+      testSpec.params = [
+        {
+          label: "param1",
+          param: "USER_PARAM1",
+        },
+        {
+          label: "param2",
+          param: "USER_PARAM2",
+        },
+      ];
+      readParamsFileStub.resolves({
+        USER_PARAM1: "val1",
+        USER_PARAM2: "val2",
+      });
+
+      expect(optionsHelper.getParams(testOptions, testSpec)).to.eventually.deep.eq({
+        ...{
+          USER_PARAM1: "val1",
+          USER_PARAM2: "val2",
+        },
+        ...autoParams,
+      });
+    });
+
+    it("should subsitute into params that reference other params", () => {
+      testSpec.params = [
+        {
+          label: "param1",
+          param: "USER_PARAM1",
+        },
+        {
+          label: "param2",
+          param: "USER_PARAM2",
+        },
+        {
+          label: "param3",
+          param: "USER_PARAM3",
+        },
+      ];
+      readParamsFileStub.resolves({
+        USER_PARAM1: "${PROJECT_ID}-hello",
+        USER_PARAM2: "val2",
+        USER_PARAM3: "${USER_PARAM2}",
+      });
+
+      expect(optionsHelper.getParams(testOptions, testSpec)).to.eventually.deep.eq({
+        ...{
+          USER_PARAM1: "test-hello",
+          USER_PARAM2: "val2",
+          USER_PARAM3: "val2",
+        },
+        ...autoParams,
+      });
+    });
+
+    it("should fallback to defaults if a value isn't provided", () => {
+      testSpec.params = [
+        {
+          label: "param1",
+          param: "USER_PARAM1",
+          default: "hi",
+        },
+        {
+          label: "param2",
+          param: "USER_PARAM2",
+          default: "hello",
+        },
+      ];
+      readParamsFileStub.resolves({});
+
+      expect(optionsHelper.getParams(testOptions, testSpec)).to.eventually.deep.eq({
+        ...{
+          USER_PARAM1: "hi",
+          USER_PARAM2: "hello",
+        },
+        ...autoParams,
+      });
+    });
+  });
+});
diff --git a/src/test/extensions/extensionsApi.spec.ts b/src/test/extensions/extensionsApi.spec.ts
index 2f4cef84d..38a257d7d 100644
--- a/src/test/extensions/extensionsApi.spec.ts
+++ b/src/test/extensions/extensionsApi.spec.ts
@@ -390,6 +390,7 @@ describe("extensions", () => {
         name: "abc123",
         version: "0.1.0",
         resources: [],
+        params: [],
         sourceUrl: "www.google.com/pack.zip",
       },
     };
diff --git a/src/test/extensions/extensionsHelper.spec.ts b/src/test/extensions/extensionsHelper.spec.ts
index 2d06b35a8..b62794c8d 100644
--- a/src/test/extensions/extensionsHelper.spec.ts
+++ b/src/test/extensions/extensionsHelper.spec.ts
@@ -28,7 +28,7 @@ describe("extensionsHelper", () => {
         },
       ];
       const testParam = { VAR_ONE: "foo", VAR_TWO: "bar", UNUSED: "faz" };
-      expect(extensionsHelper.substituteParams(testResources, testParam)).to.deep.equal([
+      expect(extensionsHelper.substituteParams<any>(testResources, testParam)).to.deep.equal([
         {
           resourceOne: {
             name: "foo",
@@ -67,7 +67,7 @@ describe("extensionsHelper", () => {
       },
     ];
     const testParam = { VAR_ONE: "foo", VAR_TWO: "bar", UNUSED: "faz" };
-    expect(extensionsHelper.substituteParams(testResources, testParam)).to.deep.equal([
+    expect(extensionsHelper.substituteParams<any>(testResources, testParam)).to.deep.equal([
       {
         resourceOne: {
           name: "foo",
@@ -105,19 +105,22 @@ describe("extensionsHelper", () => {
       ENV_VAR_FOUR: "users/{sender}.friends",
     };
 
-    const exampleParamSpec = [
+    const exampleParamSpec: extensionsApi.Param[] = [
       {
         param: "ENV_VAR_ONE",
+        label: "env1",
         required: true,
       },
       {
         param: "ENV_VAR_TWO",
+        label: "env2",
         required: true,
         validationRegex: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$",
         validationErrorMessage: "You must provide a valid email address.\n",
       },
       {
         param: "ENV_VAR_THREE",
+        label: "env3",
         default: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}",
         validationRegex: ".*\\{token\\}.*",
         validationErrorMessage:
@@ -125,6 +128,7 @@ describe("extensionsHelper", () => {
       },
       {
         param: "ENV_VAR_FOUR",
+        label: "env4",
         default: "users/{sender}.friends",
         required: false,
         validationRegex: ".+/.+\\..+",
@@ -159,19 +163,22 @@ describe("extensionsHelper", () => {
   });
 
   describe("validateCommandLineParams", () => {
-    const exampleParamSpec = [
+    const exampleParamSpec: extensionsApi.Param[] = [
       {
         param: "ENV_VAR_ONE",
+        label: "env1",
         required: true,
       },
       {
         param: "ENV_VAR_TWO",
+        label: "env2",
         required: true,
         validationRegex: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$",
         validationErrorMessage: "You must provide a valid email address.\n",
       },
       {
         param: "ENV_VAR_THREE",
+        label: "env3",
         default: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}",
         validationRegex: ".*\\{token\\}.*",
         validationErrorMessage:
@@ -179,6 +186,7 @@ describe("extensionsHelper", () => {
       },
       {
         param: "ENV_VAR_FOUR",
+        label: "env3",
         default: "users/{sender}.friends",
         required: false,
         validationRegex: ".+/.+\\..+",
@@ -212,7 +220,7 @@ describe("extensionsHelper", () => {
       }).to.throw(FirebaseError);
     });
 
-    it("should throw a error if a required param is missing", () => {
+    it("should throw an error if a required param is missing", () => {
       const testParamSpec = [
         {
           param: "HI",
@@ -410,6 +418,7 @@ describe("extensionsHelper", () => {
         version: "0.1.0",
         specVersion: "v1beta",
         resources: [],
+        params: [],
         sourceUrl: "https://test-source.fake",
         license: "apache-2.0",
       };
@@ -424,6 +433,7 @@ describe("extensionsHelper", () => {
         version: "0.1.0",
         specVersion: "v1beta",
         resources: [],
+        params: [],
         sourceUrl: "https://test-source.fake",
       };
 
@@ -437,6 +447,7 @@ describe("extensionsHelper", () => {
         version: "0.1.0",
         specVersion: "v1beta",
         resources: [],
+        params: [],
         sourceUrl: "https://test-source.fake",
         license: "invalid-license",
       };
@@ -574,22 +585,6 @@ describe("extensionsHelper", () => {
         extensionsHelper.validateSpec(testSpec);
       }).to.throw(FirebaseError, /Invalid type/);
     });
-    it("should error if a param has an invalid default.", () => {
-      const testSpec = {
-        version: "0.1.0",
-        specVersion: "v1beta",
-        params: [
-          { type: extensionsHelper.SpecParamType.STRING, validationRegex: "test", default: "fail" },
-        ],
-        resources: [],
-        sourceUrl: "https://test-source.fake",
-        license: "apache-2.0",
-      };
-
-      expect(() => {
-        extensionsHelper.validateSpec(testSpec);
-      }).to.throw(FirebaseError, /default/);
-    });
     it("should error if a param selectResource missing resourceType.", () => {
       const testSpec = {
         version: "0.1.0",
@@ -718,6 +713,7 @@ describe("extensionsHelper", () => {
         version: "0.0.0",
         sourceUrl: testUrl,
         resources: [],
+        params: [],
       },
     };
 
@@ -811,6 +807,7 @@ describe("extensionsHelper", () => {
         version: "0.0.0",
         sourceUrl: "",
         resources: [],
+        params: [],
       },
     };
 
diff --git a/src/test/extensions/paramHelper.spec.ts b/src/test/extensions/paramHelper.spec.ts
index 532b92b9f..6d5535170 100644
--- a/src/test/extensions/paramHelper.spec.ts
+++ b/src/test/extensions/paramHelper.spec.ts
@@ -82,7 +82,7 @@ describe("paramHelper", () => {
       dotenvStub = sinon.stub(dotenv, "parse");
       sinon.stub(extensionsHelper, "getFirebaseProjectParams").resolves({ PROJECT_ID });
       promptStub = sinon.stub(prompt, "promptOnce").resolves("user input");
-      loggerSpy = sinon.spy(logger, "info");
+      loggerSpy = sinon.spy(logger, "warn");
     });
 
     afterEach(() => {
diff --git a/src/test/extensions/warnings.spec.ts b/src/test/extensions/warnings.spec.ts
index 185a13c0c..a2b32d19d 100644
--- a/src/test/extensions/warnings.spec.ts
+++ b/src/test/extensions/warnings.spec.ts
@@ -16,6 +16,7 @@ const testExtensionVersion: ExtensionVersion = {
     name: "test",
     version: "0.1.0",
     resources: [],
+    params: [],
     sourceUrl: "github.com/test/meout",
   },
 };