diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 27d22f588a..8f47b4894d 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -73,6 +73,7 @@ exports[`should create default config 1`] = ` "cloneEnvironment": false, "embedProxy": false, "embedProxyFrontend": false, + "proxyReturnAllToggles": false, "responseTimeWithAppName": false, "syncSSOGroups": false, "toggleTagFiltering": false, @@ -88,6 +89,7 @@ exports[`should create default config 1`] = ` "cloneEnvironment": false, "embedProxy": false, "embedProxyFrontend": false, + "proxyReturnAllToggles": false, "responseTimeWithAppName": false, "syncSSOGroups": false, "toggleTagFiltering": false, diff --git a/src/lib/routes/index.ts b/src/lib/routes/index.ts index a0aa8024a9..276946608a 100644 --- a/src/lib/routes/index.ts +++ b/src/lib/routes/index.ts @@ -10,7 +10,7 @@ const ClientApi = require('./client-api'); const Controller = require('./controller'); import { HealthCheckController } from './health-check'; import ProxyController from './proxy-api'; -import { conditionalMiddleware } from '../middleware/conditional-middleware'; +import { conditionalMiddleware } from '../middleware'; import EdgeController from './edge-api'; import { PublicInviteController } from './public-invite'; @@ -47,7 +47,8 @@ class IndexRouter extends Controller { '/api/frontend', conditionalMiddleware( () => config.flagResolver.isEnabled('embedProxy'), - new ProxyController(config, services).router, + new ProxyController(config, services, config.flagResolver) + .router, ), ); diff --git a/src/lib/routes/proxy-api/index.ts b/src/lib/routes/proxy-api/index.ts index d68bfdc6a1..5c0d8f0313 100644 --- a/src/lib/routes/proxy-api/index.ts +++ b/src/lib/routes/proxy-api/index.ts @@ -1,21 +1,25 @@ -import { Response, Request } from 'express'; +import { Request, Response } from 'express'; import Controller from '../controller'; -import { IUnleashConfig, IUnleashServices } from '../../types'; +import { + IFlagResolver, + IUnleashConfig, + IUnleashServices, + NONE, +} from '../../types'; import { Logger } from '../../logger'; -import { NONE } from '../../types/permissions'; import ApiUser from '../../types/api-user'; import { + createRequestSchema, + createResponseSchema, + emptyResponse, + ProxyClientSchema, proxyFeaturesSchema, ProxyFeaturesSchema, -} from '../../openapi/spec/proxy-features-schema'; + ProxyMetricsSchema, +} from '../../openapi'; import { Context } from 'unleash-client'; -import { enrichContextWithIp } from '../../proxy/create-context'; -import { ProxyMetricsSchema } from '../../openapi/spec/proxy-metrics-schema'; -import { ProxyClientSchema } from '../../openapi/spec/proxy-client-schema'; -import { createResponseSchema } from '../../openapi/util/create-response-schema'; -import { createRequestSchema } from '../../openapi/util/create-request-schema'; -import { emptyResponse } from '../../openapi/util/standard-responses'; -import { corsOriginMiddleware } from '../../middleware/cors-origin-middleware'; +import { enrichContextWithIp } from '../../proxy'; +import { corsOriginMiddleware } from '../../middleware'; interface ApiUserRequest< PARAM = any, @@ -36,10 +40,17 @@ export default class ProxyController extends Controller { private services: Services; - constructor(config: IUnleashConfig, services: Services) { + private flagResolver: IFlagResolver; + + constructor( + config: IUnleashConfig, + services: Services, + flagResolver: IFlagResolver, + ) { super(config); this.logger = config.getLogger('proxy-api/index.ts'); this.services = services; + this.flagResolver = flagResolver; // Support CORS requests for the frontend endpoints. // Preflight requests are handled in `app.ts`. @@ -133,10 +144,18 @@ export default class ProxyController extends Controller { req: ApiUserRequest, res: Response, ) { - const toggles = await this.services.proxyService.getProxyFeatures( - req.user, - ProxyController.createContext(req), - ); + let toggles; + if (this.flagResolver.isEnabled('proxyReturnAllToggles')) { + toggles = await this.services.proxyService.getAllProxyFeatures( + req.user, + ProxyController.createContext(req), + ); + } else { + toggles = await this.services.proxyService.getProxyFeatures( + req.user, + ProxyController.createContext(req), + ); + } this.services.openApiService.respondWithValidation( 200, res, diff --git a/src/lib/services/proxy-service.ts b/src/lib/services/proxy-service.ts index e5d22c3253..c858c2790e 100644 --- a/src/lib/services/proxy-service.ts +++ b/src/lib/services/proxy-service.ts @@ -1,7 +1,6 @@ -import { IUnleashConfig } from '../types/option'; +import { IUnleashConfig, IUnleashServices, IUnleashStores } from '../types'; import { Logger } from '../logger'; -import { IUnleashServices, IUnleashStores } from '../types'; -import { ProxyFeatureSchema } from '../openapi/spec/proxy-feature-schema'; +import { ProxyFeatureSchema, ProxyMetricsSchema } from '../openapi'; import ApiUser from '../types/api-user'; import { Context, @@ -10,10 +9,9 @@ import { Unleash, UnleashEvents, } from 'unleash-client'; -import { ProxyRepository } from '../proxy/proxy-repository'; +import { ProxyRepository } from '../proxy'; import assert from 'assert'; import { ApiTokenType } from '../types/models/api-token'; -import { ProxyMetricsSchema } from '../openapi/spec/proxy-metrics-schema'; type Config = Pick; @@ -59,6 +57,21 @@ export class ProxyService { })); } + async getAllProxyFeatures( + token: ApiUser, + context: Context, + ): Promise { + const client = await this.clientForProxyToken(token); + const definitions = client.getFeatureToggleDefinitions() || []; + + return definitions.map((feature) => ({ + name: feature.name, + enabled: Boolean(feature.enabled), + variant: client.forceGetVariant(feature.name, context), + impressionData: Boolean(feature.impressionData), + })); + } + async registerProxyMetrics( token: ApiUser, metrics: ProxyMetricsSchema, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index ece845809d..1797833822 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -1,4 +1,4 @@ -import { parseEnvVarBoolean } from '../util/parseEnvVar'; +import { parseEnvVarBoolean } from '../util'; export type IFlags = Partial>; @@ -38,6 +38,10 @@ export const defaultExperimentalOptions = { process.env.UNLEASH_EXPERIMENTAL_TOGGLE_TAG_FILTERING, false, ), + proxyReturnAllToggles: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_PROXY_RETURN_ALL_TOGGLES, + false, + ), variantsPerEnvironment: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_VARIANTS_PER_ENVIRONMENT, false, @@ -57,6 +61,7 @@ export interface IExperimentalOptions { syncSSOGroups?: boolean; changeRequests?: boolean; cloneEnvironment?: boolean; + proxyReturnAllToggles?: boolean; variantsPerEnvironment?: boolean; }; externalResolver: IExternalFlagResolver; diff --git a/src/test/e2e/api/proxy/proxy.e2e.test.ts b/src/test/e2e/api/proxy/proxy.e2e.test.ts index 3147934957..de2c5250bd 100644 --- a/src/test/e2e/api/proxy/proxy.e2e.test.ts +++ b/src/test/e2e/api/proxy/proxy.e2e.test.ts @@ -1,16 +1,19 @@ import { IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper'; import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; -import { randomId } from '../../../../lib/util/random-id'; +import { randomId } from '../../../../lib/util'; import { ApiTokenType, IApiToken, IApiTokenCreate, } from '../../../../lib/types/models/api-token'; import { startOfHour } from 'date-fns'; -import { IConstraint, IStrategyConfig } from '../../../../lib/types/model'; -import { ProxyRepository } from '../../../../lib/proxy/proxy-repository'; -import { FEATURE_UPDATED } from '../../../../lib/types/events'; +import { + FEATURE_UPDATED, + IConstraint, + IStrategyConfig, +} from '../../../../lib/types'; +import { ProxyRepository } from '../../../../lib/proxy'; let app: IUnleashTest; let db: ITestDb; @@ -929,3 +932,52 @@ test('Should not recursively set off timers on events', async () => { expect(spy.mock.calls.length < 3).toBe(true); jest.useRealTimers(); }); + +test('should return all features when specified', async () => { + app.config.experimental.flags.proxyReturnAllToggles = true; + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); + await createFeatureToggle({ + name: 'enabledFeature1', + enabled: true, + strategies: [{ name: 'default', constraints: [], parameters: {} }], + }); + await createFeatureToggle({ + name: 'enabledFeature2', + enabled: true, + strategies: [{ name: 'default', constraints: [], parameters: {} }], + }); + await createFeatureToggle({ + name: 'disabledFeature', + enabled: false, + strategies: [{ name: 'default', constraints: [], parameters: {} }], + }); + await app.request + .get('/api/frontend') + .set('Authorization', frontendToken.secret) + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body).toEqual({ + toggles: [ + { + name: 'enabledFeature1', + enabled: true, + impressionData: false, + variant: { enabled: false, name: 'disabled' }, + }, + { + name: 'enabledFeature2', + enabled: true, + impressionData: false, + variant: { enabled: false, name: 'disabled' }, + }, + { + name: 'disabledFeature', + enabled: false, + impressionData: false, + variant: { enabled: false, name: 'disabled' }, + }, + ], + }); + }); +}); diff --git a/src/test/e2e/helpers/test-helper.ts b/src/test/e2e/helpers/test-helper.ts index 33b62b7c18..172be1cfe4 100644 --- a/src/test/e2e/helpers/test-helper.ts +++ b/src/test/e2e/helpers/test-helper.ts @@ -4,7 +4,7 @@ import supertest from 'supertest'; import EventEmitter from 'events'; import getApp from '../../../lib/app'; import { createTestConfig } from '../../config/test-config'; -import { IAuthType } from '../../../lib/types/option'; +import { IAuthType, IUnleashConfig } from '../../../lib/types/option'; import { createServices } from '../../../lib/services'; import sessionDb from '../../../lib/middleware/session-db'; import { IUnleashStores } from '../../../lib/types'; @@ -16,6 +16,7 @@ export interface IUnleashTest { request: supertest.SuperAgentTest; destroy: () => Promise; services: IUnleashServices; + config: IUnleashConfig; } async function createApp( @@ -49,7 +50,7 @@ async function createApp( }; // TODO: use create from server-impl instead? - return { request, destroy, services }; + return { request, destroy, services, config }; } export async function setupApp(stores: IUnleashStores): Promise {