From 3eaefb4deb0ab7420fb76c2a72212d5b86309a46 Mon Sep 17 00:00:00 2001 From: Chuan-kai Lin Date: Wed, 16 Jul 2025 07:06:52 -0700 Subject: [PATCH] Replicate "too many feature flags" error in test --- src/feature-flags.test.ts | 30 ++++++++++++++++++++++++++++ src/testing-utils.ts | 42 +++++++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/feature-flags.test.ts b/src/feature-flags.test.ts index 9e316214f..778f2aef5 100644 --- a/src/feature-flags.test.ts +++ b/src/feature-flags.test.ts @@ -21,6 +21,7 @@ import { mockFeatureFlagApiEndpoint, setupActionsVars, setupTests, + stubFeatureFlagApiEndpoint, } from "./testing-utils"; import { ToolsFeature } from "./tools-features"; import * as util from "./util"; @@ -132,6 +133,35 @@ test("Features use default value if they're not returned in API response", async }); }); +test("Include no more than 25 features in each API request", async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + + stubFeatureFlagApiEndpoint((request) => { + const requestedFeatures = (request.features as string).split(","); + return { + status: requestedFeatures.length <= 25 ? 200 : 400, + messageIfError: "Can request a maximum of 25 features.", + data: {}, + }; + }); + + // We only need to call getValue once, and it does not matter which feature + // we ask for. Under the hood, the features library will request all features + // from the API. + const feature = Object.values(Feature)[0]; + // TODO: change to `t.notThrowsAsync` once we implement request chunking. + await t.throwsAsync( + async () => features.getValue(feature, includeCodeQlIfRequired(feature)), + { + message: + "Encountered an error while trying to determine feature enablement: " + + "Error: Can request a maximum of 25 features.", + }, + ); + }); +}); + test("Feature flags exception is propagated if the API request errors", async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 81aebde6f..2a93c701c 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -180,6 +180,21 @@ export function getRecordingLogger(messages: LoggedMessage[]): Logger { export function mockFeatureFlagApiEndpoint( responseStatusCode: number, response: { [flagName: string]: boolean }, +) { + stubFeatureFlagApiEndpoint(() => ({ + status: responseStatusCode, + messageIfError: "some error message", + data: response, + })); +} + +/** Stub the HTTP request to the feature flags enablement API endpoint. */ +export function stubFeatureFlagApiEndpoint( + responseFunction: (params: any) => { + status: number; + messageIfError?: string; + data: { [flagName: string]: boolean }; + }, ) { // Passing an auth token is required, so we just use a dummy value const client = github.getOctokit("123"); @@ -189,16 +204,23 @@ export function mockFeatureFlagApiEndpoint( const optInSpy = requestSpy.withArgs( "GET /repos/:owner/:repo/code-scanning/codeql-action/features", ); - if (responseStatusCode < 300) { - optInSpy.resolves({ - status: responseStatusCode, - data: response, - headers: {}, - url: "GET /repos/:owner/:repo/code-scanning/codeql-action/features", - }); - } else { - optInSpy.throws(new HTTPError("some error message", responseStatusCode)); - } + + optInSpy.callsFake((_route, params) => { + const response = responseFunction(params); + if (response.status < 300) { + return Promise.resolve({ + status: response.status, + data: response.data, + headers: {}, + url: "GET /repos/:owner/:repo/code-scanning/codeql-action/features", + }); + } else { + throw new HTTPError( + response.messageIfError || "default stub error message", + response.status, + ); + } + }); sinon.stub(apiClient, "getApiClient").value(() => client); }