diff --git a/src/lib/features/playground/clean-context.test.ts b/src/lib/features/playground/clean-context.test.ts new file mode 100644 index 0000000000..bf32cacf98 --- /dev/null +++ b/src/lib/features/playground/clean-context.test.ts @@ -0,0 +1,35 @@ +import { cleanContext } from './clean-context'; + +test('strips invalid context properties from the context', async () => { + const invalidJsonTypes = { + object: {}, + array: [], + true: true, + false: false, + number: 123, + null: null, + }; + + const validValues = { + appName: 'test', + }; + + const inputContext = { + ...invalidJsonTypes, + ...validValues, + }; + + const cleanedContext = cleanContext(inputContext); + + expect(cleanedContext).toStrictEqual(validValues); +}); + +test("doesn't add non-existing properties", async () => { + const input = { + appName: 'test', + }; + + const output = cleanContext(input); + + expect(output).toStrictEqual(input); +}); diff --git a/src/lib/features/playground/clean-context.ts b/src/lib/features/playground/clean-context.ts new file mode 100644 index 0000000000..238c0b0960 --- /dev/null +++ b/src/lib/features/playground/clean-context.ts @@ -0,0 +1,16 @@ +import type { SdkContextSchema } from '../../openapi'; + +export const cleanContext = (context: SdkContextSchema): SdkContextSchema => { + const { appName, ...otherContextFields } = context; + + const cleanedContextFields = Object.fromEntries( + Object.entries(otherContextFields).filter( + ([key, value]) => key === 'properties' || typeof value === 'string', + ), + ); + + return { + ...cleanedContextFields, + appName, + }; +}; diff --git a/src/lib/features/playground/playground-api.e2e.test.ts b/src/lib/features/playground/playground-api.e2e.test.ts new file mode 100644 index 0000000000..fd2d747cec --- /dev/null +++ b/src/lib/features/playground/playground-api.e2e.test.ts @@ -0,0 +1,78 @@ +import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; +import { + type IUnleashTest, + setupApp, +} from '../../../test/e2e/helpers/test-helper'; +import type { IUnleashStores } from '../../types'; +import getLogger from '../../../test/fixtures/no-logger'; + +let stores: IUnleashStores; +let db: ITestDb; +let app: IUnleashTest; + +const flag = { + name: 'test-flag', + enabled: true, + strategies: [{ name: 'default' }], + createdByUserId: 9999, +}; + +beforeAll(async () => { + db = await dbInit('playground_api', getLogger); + stores = db.stores; + + await stores.featureToggleStore.create('default', flag); + + app = await setupApp(stores); +}); + +afterAll(async () => { + await db.destroy(); +}); + +test('strips invalid context properties from input before using it', async () => { + const validData = { + appName: 'test', + }; + + const inputContext = { + invalid: {}, + ...validData, + }; + + const { body } = await app.request + .post('/api/admin/playground/advanced') + .send({ + context: inputContext, + environments: ['production'], + projects: '*', + }) + .expect(200); + + const evaluatedContext = + body.features[0].environments.production[0].context; + + expect(evaluatedContext).toStrictEqual(validData); +}); + +test('returns the input context exactly as it came in, even if invalid values have been removed for the evaluation', async () => { + const invalidData = { + invalid: {}, + }; + + const inputContext = { + ...invalidData, + appName: 'test', + }; + + const { body } = await app.request + .post('/api/admin/playground/advanced') + .send({ + context: inputContext, + environments: ['production'], + projects: '*', + }) + .expect(200); + + expect(body.input.context).toMatchObject(inputContext); +}); diff --git a/src/lib/features/playground/playground-service.ts b/src/lib/features/playground/playground-service.ts index 1f20632580..a04b05828a 100644 --- a/src/lib/features/playground/playground-service.ts +++ b/src/lib/features/playground/playground-service.ts @@ -28,6 +28,7 @@ import type { AdvancedPlaygroundEnvironmentFeatureSchema } from '../../openapi/s import { validateQueryComplexity } from './validateQueryComplexity'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; import { getDefaultVariant } from './feature-evaluator/variant'; +import { cleanContext } from './clean-context'; type EvaluationInput = { features: FeatureConfigurationClient[]; @@ -124,7 +125,8 @@ export class PlaygroundService { this.resolveFeatures(filteredProjects, env), ), ); - const contexts = generateObjectCombinations(context); + + const contexts = generateObjectCombinations(cleanContext(context)); validateQueryComplexity( environments.length,