From e8c817cbb7a4ffc8493c547491714aa9df6cc9c6 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 13 Apr 2021 14:48:23 -0700 Subject: [PATCH] Add strong typing to context, options, and paylaod in Functions codebase (#3271) What it says in the box. The nebulous context: any, options: any, payload: any are now strongly typed. To get part of this working I had to convert strings to Runtimes. I also removed a few lodash calls that I saw. --- src/deploy/functions/args.ts | 79 +++++++++++++++++++++ src/deploy/functions/checkIam.ts | 11 ++- src/deploy/functions/deploy.ts | 45 +++++++----- src/deploy/functions/prepare.ts | 26 +++++-- src/deploy/functions/prompts.ts | 4 +- src/deploy/functions/release.ts | 15 ++-- src/deploy/functions/tasks.ts | 3 +- src/deploy/functions/validate.ts | 8 ++- src/functionsDeployHelper.ts | 9 +-- src/parseRuntimeAndValidateSDK.ts | 14 ++-- src/prepareFunctionsUpload.ts | 20 +++--- src/test/deploy/functions/prompts.spec.ts | 56 +++++++++------ src/test/functionsDeployHelper.spec.ts | 5 +- src/test/parseRuntimeAndValidateSDK.spec.ts | 1 + 14 files changed, 219 insertions(+), 77 deletions(-) create mode 100644 src/deploy/functions/args.ts diff --git a/src/deploy/functions/args.ts b/src/deploy/functions/args.ts new file mode 100644 index 00000000000..2d9384b220a --- /dev/null +++ b/src/deploy/functions/args.ts @@ -0,0 +1,79 @@ +// These types should proably be in a root deploy.ts, but we can only boil the ocean one bit at a time. + +import { ReadStream } from "fs"; +import * as gcf from "../../gcp/cloudfunctions"; +import * as deploymentPlanner from "./deploymentPlanner"; + +// Payload holds the output types of what we're building. +export interface Payload { + functions?: { + byRegion: deploymentPlanner.RegionMap; + triggers: deploymentPlanner.CloudFunctionTrigger[]; + }; +} + +// Options come from command-line options and stored config values +// TODO: actually define all of this stuff in command.ts and import it from there. +export interface Options { + cwd: string; + configPath: string; + + // OMITTED: project. Use context.projectId instead + + only: string; + + // defined in /config.js + config: { + // Note: it might be worth defining overloads for config values we use in + // deploy/functions. + get(key: string, defaultValue?: any): any; + set(key: string, value: any): void; + has(key: string): boolean; + path(pathName: string): string; + + // I/O methods: these methods work with JSON objects. + // WARNING: they all use synchronous I/O + readProjectFile(file: string): unknown; + writeProjectFile(path: string, content: unknown): void; + askWriteProjectFile(path: string, content: unknown): void; + + projectDir: string; + }; + filteredTargets: string[]; + nonInteractive: boolean; + force: boolean; +} + +export interface FunctionsSource { + file: string; + stream: ReadStream; + size: number; +} + +// Context holds cached values of what we've looked up in handling this request. +// For non-trivial values, use helper functions that cache automatically and/or hide implementation +// details. +export interface Context { + projectId: string; + filters: string[][]; + + // Filled in the "prepare" phase. + functionsSource?: FunctionsSource; + // TODO: replace with backend.Runtime once it is committed. + runtimeChoice?: gcf.Runtime; + runtimeConfigEnabled?: boolean; + firebaseConfig?: FirebaseConfig; + + // Filled in the "deploy" phase. + uploadUrl?: string; + + // TOOD: move to caching function w/ helper + existingFunctions?: gcf.CloudFunction[]; +} + +export interface FirebaseConfig { + locationId: string; + projectId: string; + storageBucket: string; + databaseURL: string; +} diff --git a/src/deploy/functions/checkIam.ts b/src/deploy/functions/checkIam.ts index 8b151ffa00b..10ad848f691 100644 --- a/src/deploy/functions/checkIam.ts +++ b/src/deploy/functions/checkIam.ts @@ -7,6 +7,7 @@ import { getReleaseNames, getFilterGroups } from "../../functionsDeployHelper"; import { CloudFunctionTrigger } from "./deploymentPlanner"; import { FirebaseError } from "../../error"; import { testIamPermissions, testResourceIamPermissions } from "../../gcp/iam"; +import * as args from "./args"; const PERMISSION = "cloudfunctions.functions.setIamPolicy"; @@ -51,15 +52,19 @@ export async function checkServiceAccountIam(projectId: string): Promise { * @param options The command-wide options object. * @param payload The deploy payload. */ -export async function checkHttpIam(context: any, options: any, payload: any): Promise { - const functionsInfo = payload.functions.triggers; +export async function checkHttpIam( + context: args.Context, + options: args.Options, + payload: args.Payload +): Promise { + const functionsInfo = payload.functions!.triggers; const filterGroups = context.filters || getFilterGroups(options); const httpFunctionNames: string[] = functionsInfo .filter((f: CloudFunctionTrigger) => has(f, "httpsTrigger")) .map((f: CloudFunctionTrigger) => f.name); const httpFunctionFullNames: string[] = getReleaseNames(httpFunctionNames, [], filterGroups); - const existingFunctionFullNames: string[] = context.existingFunctions.map( + const existingFunctionFullNames: string[] = context.existingFunctions!.map( (f: { name: string }) => f.name ); diff --git a/src/deploy/functions/deploy.ts b/src/deploy/functions/deploy.ts index b32658963d5..b51c2cb1054 100644 --- a/src/deploy/functions/deploy.ts +++ b/src/deploy/functions/deploy.ts @@ -5,12 +5,13 @@ import { functionsUploadRegion } from "../../api"; import * as gcp from "../../gcp"; import { logSuccess, logWarning } from "../../utils"; import { checkHttpIam } from "./checkIam"; +import * as args from "./args"; const GCP_REGION = functionsUploadRegion; setGracefulCleanup(); -async function uploadSource(context: any): Promise { +async function uploadSource(context: args.Context): Promise { const uploadUrl = await gcp.cloudfunctions.generateUploadUrl(context.projectId, GCP_REGION); context.uploadUrl = uploadUrl; const apiUploadUrl = uploadUrl.replace("https://storage.googleapis.com", ""); @@ -24,24 +25,30 @@ async function uploadSource(context: any): Promise { * @param payload The deploy payload. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function deploy(context: any, options: any, payload: any): Promise { - if (options.config.get("functions")) { - await checkHttpIam(context, options, payload); +export async function deploy( + context: args.Context, + options: args.Options, + payload: args.Payload +): Promise { + if (!options.config.get("functions")) { + return; + } + + await checkHttpIam(context, options, payload); - if (!context.functionsSource) { - return; - } - try { - await uploadSource(context); - logSuccess( - clc.green.bold("functions:") + - " " + - clc.bold(options.config.get("functions.source")) + - " folder uploaded successfully" - ); - } catch (err) { - logWarning(clc.yellow("functions:") + " Upload Error: " + err.message); - throw err; - } + if (!context.functionsSource) { + return; + } + try { + await uploadSource(context); + logSuccess( + clc.green.bold("functions:") + + " " + + clc.bold(options.config.get("functions.source")) + + " folder uploaded successfully" + ); + } catch (err) { + logWarning(clc.yellow("functions:") + " Upload Error: " + err.message); + throw err; } } diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index 4977978af85..4873c71c84a 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -9,13 +9,18 @@ import { functionMatchesAnyGroup, getFilterGroups } from "../../functionsDeployH import { CloudFunctionTrigger, functionsByRegion, allFunctions } from "./deploymentPlanner"; import { promptForFailurePolicies } from "./prompts"; import { prepareFunctionsUpload } from "../../prepareFunctionsUpload"; +import * as args from "./args"; import * as gcp from "../../gcp"; import * as validate from "./validate"; import { checkRuntimeDependencies } from "./checkRuntimeDependencies"; import { FirebaseError } from "../../error"; -export async function prepare(context: any, options: any, payload: any): Promise { +export async function prepare( + context: args.Context, + options: args.Options, + payload: args.Payload +): Promise { if (!options.config.has("functions")) { return; } @@ -31,9 +36,14 @@ export async function prepare(context: any, options: any, payload: any): Promise // Check that all necessary APIs are enabled. const checkAPIsEnabled = await Promise.all([ - ensureApiEnabled.ensure(options.project, "cloudfunctions.googleapis.com", "functions"), - ensureApiEnabled.check(projectId, "runtimeconfig.googleapis.com", "runtimeconfig", true), - checkRuntimeDependencies(projectId, context.runtimeChoice), + ensureApiEnabled.ensure(projectId, "cloudfunctions.googleapis.com", "functions"), + ensureApiEnabled.check( + projectId, + "runtimeconfig.googleapis.com", + "runtimeconfig", + /* silent=*/ true + ), + checkRuntimeDependencies(projectId, context.runtimeChoice!), ]); context.runtimeConfigEnabled = checkAPIsEnabled[1]; @@ -70,11 +80,13 @@ export async function prepare(context: any, options: any, payload: any): Promise } // Build a regionMap, and duplicate functions for each region they are being deployed to. - payload.functions = {}; // TODO: Make byRegion an implementation detail of deploymentPlanner // and only store a flat array of Functions in payload. - payload.functions.byRegion = functionsByRegion(projectId, functions); - payload.functions.triggers = allFunctions(payload.functions.byRegion); + const byRegion = functionsByRegion(projectId, functions); + payload.functions = { + byRegion, + triggers: allFunctions(byRegion), + }; // Validate the function code that is being deployed. validate.functionsDirectoryExists(options, sourceDirName); diff --git a/src/deploy/functions/prompts.ts b/src/deploy/functions/prompts.ts index 6a2686191ce..abb9a5c1d83 100644 --- a/src/deploy/functions/prompts.ts +++ b/src/deploy/functions/prompts.ts @@ -7,6 +7,8 @@ import { promptOnce } from "../../prompt"; import { CloudFunction } from "../../gcp/cloudfunctions"; import * as utils from "../../utils"; import { logger } from "../../logger"; +import * as args from "./args"; +import * as gcf from "../../gcp/cloudfunctions"; /** * Checks if a deployment will create any functions with a failure policy. @@ -15,7 +17,7 @@ import { logger } from "../../logger"; * @param functions A list of all functions in the deployment */ export async function promptForFailurePolicies( - options: any, + options: args.Options, functions: CloudFunctionTrigger[], existingFunctions: CloudFunction[] ): Promise { diff --git a/src/deploy/functions/release.ts b/src/deploy/functions/release.ts index 643569a94d1..2efbb4a1ece 100644 --- a/src/deploy/functions/release.ts +++ b/src/deploy/functions/release.ts @@ -12,22 +12,29 @@ import { promptForFunctionDeletion } from "./prompts"; import Queue from "../../throttler/queue"; import { DeploymentTimer } from "./deploymentTimer"; import { ErrorHandler } from "./errorHandler"; +import * as args from "./args"; +import * as deploymentPlanner from "./deploymentPlanner"; -export async function release(context: any, options: any, payload: any) { +export async function release(context: args.Context, options: args.Options, payload: args.Payload) { if (!options.config.has("functions")) { return; } const projectId = context.projectId; - const sourceUrl = context.uploadUrl; + const sourceUrl = context.uploadUrl!; const appEngineLocation = getAppEngineLocation(context.firebaseConfig); const timer = new DeploymentTimer(); const errorHandler = new ErrorHandler(); const fullDeployment = createDeploymentPlan( - payload.functions.byRegion, - context.existingFunctions, + payload.functions!.byRegion, + + // Note: this is obviously a sketchy looking cast. And it's true; the shapes don't + // line up. But it just so happens that we don't hit any bugs with the current + // implementation of the function. This will all go away once everything uses + // backend.FunctionSpec. + (context.existingFunctions! as any) as deploymentPlanner.CloudFunctionTrigger[], context.filters ); diff --git a/src/deploy/functions/tasks.ts b/src/deploy/functions/tasks.ts index e720e6f3f37..1cd279538d0 100644 --- a/src/deploy/functions/tasks.ts +++ b/src/deploy/functions/tasks.ts @@ -4,6 +4,7 @@ import { logger } from "../../logger"; import * as utils from "../../utils"; import { CloudFunctionTrigger } from "./deploymentPlanner"; import { cloudfunctions, cloudscheduler } from "../../gcp"; +import { Runtime } from "../../gcp/cloudfunctions"; import * as deploymentTool from "../../deploymentTool"; import * as helper from "../../functionsDeployHelper"; import { RegionalDeployment } from "./deploymentPlanner"; @@ -39,7 +40,7 @@ export interface DeploymentTask { export interface TaskParams { projectId: string; - runtime?: string; + runtime?: Runtime; sourceUrl?: string; errorHandler: ErrorHandler; } diff --git a/src/deploy/functions/validate.ts b/src/deploy/functions/validate.ts index 2a2f880cfbe..8de907f0039 100644 --- a/src/deploy/functions/validate.ts +++ b/src/deploy/functions/validate.ts @@ -15,11 +15,15 @@ const cjson = require("cjson"); /** * Check that functions directory exists. - * @param options options object. + * @param options options object. In prod is an args.Options; in tests can just be {cwd: string} * @param sourceDirName Relative path to source directory. * @throws { FirebaseError } Functions directory must exist. */ -export function functionsDirectoryExists(options: object, sourceDirName: string): void { +export function functionsDirectoryExists( + options: { cwd: string; configPath?: string }, + sourceDirName: string +): void { + // Note(inlined): What's the difference between this and options.config.path(sourceDirName)? if (!fsutils.dirExistsSync(projectPath.resolveProjectPath(options, sourceDirName))) { const msg = `could not deploy functions because the ${clc.bold('"' + sourceDirName + '"')} ` + diff --git a/src/functionsDeployHelper.ts b/src/functionsDeployHelper.ts index 6abfb7ad9c9..973ef6e27d1 100644 --- a/src/functionsDeployHelper.ts +++ b/src/functionsDeployHelper.ts @@ -10,6 +10,7 @@ import { Job } from "./gcp/cloudscheduler"; import { CloudFunctionTrigger } from "./deploy/functions/deploymentPlanner"; import Queue from "./throttler/queue"; import { ErrorHandler } from "./deploy/functions/errorHandler"; +import * as args from "./deploy/functions/args"; export function functionMatchesAnyGroup(fnName: string, filterGroups: string[][]) { if (!filterGroups.length) { @@ -32,21 +33,21 @@ export function functionMatchesGroup(functionName: string, groupChunks: string[] return _.isEqual(groupChunks, functionNameChunks); } -export function getFilterGroups(options: any): string[][] { +export function getFilterGroups(options: args.Options): string[][] { if (!options.only) { return []; } let opts; - return _.chain(options.only.split(",")) + return options.only + .split(",") .filter((filter) => { opts = filter.split(":"); return opts[0] === "functions" && opts[1]; }) .map((filter) => { return filter.split(":")[1].split(/[.-]/); - }) - .value(); + }); } export function getReleaseNames( diff --git a/src/parseRuntimeAndValidateSDK.ts b/src/parseRuntimeAndValidateSDK.ts index 7be80c470d6..c6aabbe5a4f 100644 --- a/src/parseRuntimeAndValidateSDK.ts +++ b/src/parseRuntimeAndValidateSDK.ts @@ -8,12 +8,13 @@ import { FirebaseError } from "./error"; import * as utils from "./utils"; import { logger } from "./logger"; import * as track from "./track"; +import { Runtime } from "./gcp/cloudfunctions"; // have to require this because no @types/cjson available // eslint-disable-next-line @typescript-eslint/no-var-requires const cjson = require("cjson"); -const MESSAGE_FRIENDLY_RUNTIMES: { [key: string]: string } = { +const MESSAGE_FRIENDLY_RUNTIMES: Record = { nodejs6: "Node.js 6 (Deprecated)", nodejs8: "Node.js 8 (Deprecated)", nodejs10: "Node.js 10", @@ -21,7 +22,7 @@ const MESSAGE_FRIENDLY_RUNTIMES: { [key: string]: string } = { nodejs14: "Node.js 14 (Beta)", }; -const ENGINE_RUNTIMES: { [key: string]: string } = { +const ENGINE_RUNTIMES: Record = { 6: "nodejs6", 8: "nodejs8", 10: "nodejs10", @@ -87,11 +88,11 @@ function functionsSDKTooOld(sourceDir: string, minRange: string): boolean { * @param runtime name of runtime in raw format, ie, "nodejs8" or "nodejs10" * @return A human-friendly string describing the runtime. */ -export function getHumanFriendlyRuntimeName(runtime: string): string { +export function getHumanFriendlyRuntimeName(runtime: Runtime): string { return _.get(MESSAGE_FRIENDLY_RUNTIMES, runtime, runtime); } -function getRuntimeChoiceFromPackageJson(sourceDir: string): string { +function getRuntimeChoiceFromPackageJson(sourceDir: string): Runtime { const packageJsonPath = path.join(sourceDir, "package.json"); const loaded = cjson.load(packageJsonPath); const engines = loaded.engines; @@ -111,7 +112,7 @@ function getRuntimeChoiceFromPackageJson(sourceDir: string): string { * @param runtimeFromConfig runtime from the `functions` section of firebase.json file (may be empty). * @return The runtime, e.g. `nodejs12`. */ -export function getRuntimeChoice(sourceDir: string, runtimeFromConfig?: string): string { +export function getRuntimeChoice(sourceDir: string, runtimeFromConfig?: Runtime | ""): Runtime { const runtime = runtimeFromConfig || getRuntimeChoiceFromPackageJson(sourceDir); const errorMessage = (runtimeFromConfig @@ -123,6 +124,9 @@ export function getRuntimeChoice(sourceDir: string, runtimeFromConfig?: string): throw new FirebaseError(errorMessage, { exit: 1 }); } + // NOTE: We could consider removing nodejs6 and nodejs8 from cloudfunctions.Runtimes and + // make the methods here take a Runtime | RemovedRuntime. Then we'd throw if it is a RemovedRuntime + // and only forward a valid Runtime. if (["nodejs6", "nodejs8"].includes(runtime)) { track("functions_runtime_notices", `${runtime}_deploy_prohibited`); throw new FirebaseError(errorMessage, { exit: 1 }); diff --git a/src/prepareFunctionsUpload.ts b/src/prepareFunctionsUpload.ts index ee5ed51dabd..2fcffe024b1 100644 --- a/src/prepareFunctionsUpload.ts +++ b/src/prepareFunctionsUpload.ts @@ -13,17 +13,22 @@ import { logger } from "./logger"; import * as utils from "./utils"; import * as parseTriggers from "./parseTriggers"; import * as fsAsync from "./fsAsync"; +import * as args from "./deploy/functions/args"; const CONFIG_DEST_FILE = ".runtimeconfig.json"; -async function getFunctionsConfig(context: any): Promise<{ [key: string]: any }> { - let config = {}; +async function getFunctionsConfig(context: args.Context): Promise<{ [key: string]: any }> { + let config: Record = {}; if (context.runtimeConfigEnabled) { try { - config = await functionsConfig.materializeAll(context.firebaseConfig.projectId); + config = await functionsConfig.materializeAll(context.firebaseConfig!.projectId); } catch (err) { logger.debug(err); - const errorCode = _.get(err, "context.response.statusCode"); + let errorCode = err?.context?.response?.statusCode; + if (!errorCode) { + logger.debug("Got unexpected error from Runtime Config; it has no status code:", err); + errorCode = 500; + } if (errorCode === 500 || errorCode === 503) { throw new FirebaseError( "Cloud Runtime Config is currently experiencing issues, " + @@ -36,8 +41,7 @@ async function getFunctionsConfig(context: any): Promise<{ [key: string]: any }> } } - const firebaseConfig = _.get(context, "firebaseConfig"); - _.set(config, "firebase", firebaseConfig); + config.firebase = context.firebaseConfig; return config; } @@ -49,7 +53,7 @@ async function pipeAsync(from: archiver.Archiver, to: fs.WriteStream) { }); } -async function packageSource(options: any, sourceDir: string, configValues: any) { +async function packageSource(options: args.Options, sourceDir: string, configValues: any) { const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix: ".zip" }).name; const fileStream = fs.createWriteStream(tmpFile, { flags: "w", @@ -105,7 +109,7 @@ async function packageSource(options: any, sourceDir: string, configValues: any) }; } -export async function prepareFunctionsUpload(context: any, options: any) { +export async function prepareFunctionsUpload(context: args.Context, options: args.Options) { const sourceDir = options.config.path(options.config.get("functions.source")); const configValues = await getFunctionsConfig(context); const triggers = await parseTriggers(getProjectId(options), sourceDir, configValues); diff --git a/src/test/deploy/functions/prompts.spec.ts b/src/test/deploy/functions/prompts.spec.ts index c79e4184921..5fe1a3ed80e 100644 --- a/src/test/deploy/functions/prompts.spec.ts +++ b/src/test/deploy/functions/prompts.spec.ts @@ -5,8 +5,14 @@ import * as prompt from "../../../prompt"; import * as functionPrompts from "../../../deploy/functions/prompts"; import { FirebaseError } from "../../../error"; import { CloudFunctionTrigger } from "../../../deploy/functions/deploymentPlanner"; -import * as gcp from "../../../gcp"; import * as gcf from "../../../gcp/cloudfunctions"; +import * as args from "../../../deploy/functions/args"; + +// Dropping unused fields intentionally +const SAMPLE_OPTIONS: args.Options = ({ + nonInteractive: false, + force: false, +} as any) as args.Options; describe("promptForFailurePolicies", () => { let promptStub: sinon.SinonStub; @@ -14,13 +20,21 @@ describe("promptForFailurePolicies", () => { beforeEach(() => { promptStub = sinon.stub(prompt, "promptOnce"); + existingFunctions = []; }); afterEach(() => { promptStub.restore(); - existingFunctions = []; }); + // Note: Context is used for caching values, so it must be reset between each test. + function newContext(): args.Context { + return { + projectId: "a", + filters: [], + }; + } + it("should prompt if there are new functions with failure policies", async () => { const funcs: CloudFunctionTrigger[] = [ { @@ -31,31 +45,34 @@ describe("promptForFailurePolicies", () => { failurePolicy: {}, }, ]; - const options = {}; promptStub.resolves(true); - await expect(functionPrompts.promptForFailurePolicies(options, funcs, [])).not.to.be.rejected; + await expect(functionPrompts.promptForFailurePolicies(SAMPLE_OPTIONS, funcs, [])).not.to.be + .rejected; expect(promptStub).to.have.been.calledOnce; }); it("should not prompt if all functions with failure policies already had failure policies", async () => { // Note: local definitions of function triggers use a top-level "failurePolicy" but // the API returns eventTrigger.failurePolicy. - const func: any = { + const func = { name: "projects/a/locations/b/functions/c", entryPoint: "", labels: {}, environmentVariables: {}, failurePolicy: {}, eventTrigger: { + eventType: "eventType", + resource: "resource", failurePolicy: {}, }, + runtime: "nodejs14" as gcf.Runtime, }; - existingFunctions = [func]; - const options = {}; + existingFunctions = [func as any]; - await expect(functionPrompts.promptForFailurePolicies(options, [func], existingFunctions)).to - .eventually.be.fulfilled; + await expect( + functionPrompts.promptForFailurePolicies(SAMPLE_OPTIONS, [func], existingFunctions) + ).to.eventually.be.fulfilled; expect(promptStub).to.not.have.been.called; }); @@ -69,11 +86,10 @@ describe("promptForFailurePolicies", () => { failurePolicy: {}, }, ]; - const options = {}; promptStub.resolves(false); await expect( - functionPrompts.promptForFailurePolicies(options, funcs, []) + functionPrompts.promptForFailurePolicies(SAMPLE_OPTIONS, funcs, []) ).to.eventually.be.rejectedWith(FirebaseError, /Deployment canceled/); expect(promptStub).to.have.been.calledOnce; }); @@ -95,12 +111,11 @@ describe("promptForFailurePolicies", () => { }), ]; const newFunc = Object.assign({}, func, { failurePolicy: {} }); - const options = {}; - const context = {}; promptStub.resolves(true); - await expect(functionPrompts.promptForFailurePolicies(options, [newFunc], existingFunctions)).to - .eventually.be.fulfilled; + await expect( + functionPrompts.promptForFailurePolicies(SAMPLE_OPTIONS, [newFunc], existingFunctions) + ).to.eventually.be.fulfilled; expect(promptStub).to.have.been.calledOnce; }); @@ -119,7 +134,7 @@ describe("promptForFailurePolicies", () => { promptStub.resolves(false); await expect( - functionPrompts.promptForFailurePolicies(options, funcs, existingFunctions) + functionPrompts.promptForFailurePolicies(SAMPLE_OPTIONS, funcs, existingFunctions) ).to.eventually.be.rejectedWith(FirebaseError, /Deployment canceled/); expect(promptStub).to.have.been.calledOnce; }); @@ -133,11 +148,10 @@ describe("promptForFailurePolicies", () => { environmentVariables: {}, }, ]; - const options = {}; promptStub.resolves(); - await expect(functionPrompts.promptForFailurePolicies(options, funcs, [])).to.eventually.be - .fulfilled; + await expect(functionPrompts.promptForFailurePolicies(SAMPLE_OPTIONS, funcs, [])).to.eventually + .be.fulfilled; expect(promptStub).not.to.have.been.called; }); @@ -151,7 +165,7 @@ describe("promptForFailurePolicies", () => { failurePolicy: {}, }, ]; - const options = { nonInteractive: true }; + const options = { ...SAMPLE_OPTIONS, nonInteractive: true }; await expect(functionPrompts.promptForFailurePolicies(options, funcs, [])).to.be.rejectedWith( FirebaseError, @@ -170,7 +184,7 @@ describe("promptForFailurePolicies", () => { failurePolicy: {}, }, ]; - const options = { nonInteractive: true, force: true }; + const options = { ...SAMPLE_OPTIONS, nonInteractive: true, force: true }; await expect(functionPrompts.promptForFailurePolicies(options, funcs, [])).to.eventually.be .fulfilled; diff --git a/src/test/functionsDeployHelper.spec.ts b/src/test/functionsDeployHelper.spec.ts index ed8fa80366f..fe1d105c2b4 100644 --- a/src/test/functionsDeployHelper.spec.ts +++ b/src/test/functionsDeployHelper.spec.ts @@ -4,19 +4,20 @@ import * as sinon from "sinon"; import * as helper from "../functionsDeployHelper"; import * as prompt from "../prompt"; import { FirebaseError } from "../error"; +import * as args from "../deploy/functions/args"; describe("functionsDeployHelper", () => { describe("getFilterGroups", () => { it("should parse multiple filters", () => { const options = { only: "functions:myFunc,functions:myOtherFunc", - }; + } as args.Options; expect(helper.getFilterGroups(options)).to.deep.equal([["myFunc"], ["myOtherFunc"]]); }); it("should parse nested filters", () => { const options = { only: "functions:groupA.myFunc", - }; + } as args.Options; expect(helper.getFilterGroups(options)).to.deep.equal([["groupA", "myFunc"]]); }); }); diff --git a/src/test/parseRuntimeAndValidateSDK.spec.ts b/src/test/parseRuntimeAndValidateSDK.spec.ts index 4214ec202d5..9eacd33242b 100644 --- a/src/test/parseRuntimeAndValidateSDK.spec.ts +++ b/src/test/parseRuntimeAndValidateSDK.spec.ts @@ -76,6 +76,7 @@ describe("getRuntimeChoice", () => { }); it("should throw error if unsupported node version set", () => { + // @ts-expect-error Known invalid Runtime expect(() => runtime.getRuntimeChoice("path/to/source", "nodejs11")).to.throw( FirebaseError, runtime.UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG