diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 22ed1e21a..64a62175a 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -10,6 +10,7 @@ import test, { import nock from "nock"; import * as sinon from "sinon"; +import { ActionState, StateFeature } from "./action-common"; import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util"; import { AnalysisKind } from "./analyses"; import * as apiClient from "./api-client"; @@ -188,6 +189,99 @@ export function getTestActionsEnv(): ActionsEnv { }; } +/** For testing purposes, we make all available state features accessible in `TestEnv`. */ +type AllState = ["Logger", "Env", "FeatureFlags"]; + +/** + * Wraps a function that accepts an `ActionState` for testing in different environments. + */ +export class TestEnv< + Args extends readonly any[], + R, + Fs extends ReadonlyArray, +> { + private readonly fn: (state: ActionState, ...args: Args) => R; + private args?: Args; + private logger: RecordingLogger; + private state: ActionState; + + constructor( + fn: (state: ActionState, ...args: Args) => R, + cloneFrom?: TestEnv, + ) { + this.fn = fn; + this.args = cloneFrom?.args; + this.logger = new RecordingLogger(); + this.state = + cloneFrom !== undefined + ? { ...cloneFrom.state } + : { + logger: new RecordingLogger(), + env: getTestEnv(), + features: createFeatures([]), + }; + } + + private clone(): TestEnv { + return new TestEnv(this.fn, this); + } + + public getLogger(): RecordingLogger { + return this.logger; + } + + public getState(): ActionState { + return this.state; + } + + public getArgs(): Args | undefined { + return this.args; + } + + public withArgs(...args: Args) { + const result = this.clone(); + result.args = args; + return result; + } + + public withFeatures(enabled: Feature[]): TestEnv { + const result = this.clone(); + result.state.features = createFeatures(enabled); + return result; + } + + public withEnv(env: Env): TestEnv { + const result = this.clone(); + result.state.env = env; + return result; + } + + call(): R { + if (!this.args) { + throw new Error("Trying to call function in TestEnv without arguments."); + } + return this.fn(this.state as ActionState, ...this.args); + } + + public passes( + assertion: (makeCall: () => R) => T | Promise, + ): T | Promise { + return assertion(() => { + const result = this.call(); + return result; + }); + } +} + +/** Utility function to construct a `TestEnv`. */ +export function callee< + Args extends readonly any[], + R, + Fs extends readonly StateFeature[], +>(fn: (state: ActionState, ...args: Args) => R): TestEnv { + return new TestEnv(fn); +} + /** * Default values for environment variables typically set in an Actions * environment. Tests can override individual variables by passing them in the