diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..587f74b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,92 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main", "Preview", "Release" ] + pull_request: + branches: [ "main", "Preview", "Release" ] + schedule: + - cron: '45 7 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/sdk/feature-management/src/featureManager.ts b/sdk/feature-management/src/featureManager.ts index ac0503e..a035b5d 100644 --- a/sdk/feature-management/src/featureManager.ts +++ b/sdk/feature-management/src/featureManager.ts @@ -3,7 +3,7 @@ import { TimeWindowFilter } from "./filter/TimeWindowFilter.js"; import { IFeatureFilter } from "./filter/FeatureFilter.js"; -import { FeatureFlag, RequirementType, VariantDefinition } from "./model.js"; +import { FeatureFlag, RequirementType, VariantDefinition } from "./schema/model.js"; import { IFeatureFlagProvider } from "./featureProvider.js"; import { TargetingFilter } from "./filter/TargetingFilter.js"; import { Variant } from "./variant/Variant.js"; diff --git a/sdk/feature-management/src/featureProvider.ts b/sdk/feature-management/src/featureProvider.ts index 0f90ce6..4860421 100644 --- a/sdk/feature-management/src/featureProvider.ts +++ b/sdk/feature-management/src/featureProvider.ts @@ -2,7 +2,8 @@ // Licensed under the MIT license. import { IGettable } from "./gettable.js"; -import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model.js"; +import { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./schema/model.js"; +import { validateFeatureFlag } from "./schema/validator.js"; export interface IFeatureFlagProvider { /** @@ -28,12 +29,16 @@ export class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider } async getFeatureFlag(featureName: string): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName); + const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - return featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + validateFeatureFlag(featureFlag); + return featureFlag; } } @@ -49,10 +54,14 @@ export class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvi async getFeatureFlag(featureName: string): Promise { const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY]; - return featureFlags?.find((feature: FeatureFlag) => feature.id === featureName); + const featureFlag = featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName); + validateFeatureFlag(featureFlag); + return featureFlag; } async getFeatureFlags(): Promise { - return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + const featureFlag = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + validateFeatureFlag(featureFlag); + return featureFlag; } } diff --git a/sdk/feature-management/src/model.ts b/sdk/feature-management/src/model.ts deleted file mode 100644 index ccdb9d5..0000000 --- a/sdk/feature-management/src/model.ts +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -// Converted from: -// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json - -/** - * A feature flag is a named property that can be toggled to enable or disable some feature of an application. - */ -export interface FeatureFlag { - /** - * An ID used to uniquely identify and reference the feature. - */ - id: string; - /** - * A description of the feature. - */ - description?: string; - /** - * A display name for the feature to use for display rather than the ID. - */ - display_name?: string; - /** - * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied. - */ - enabled?: boolean; - /** - * The declaration of conditions used to dynamically enable the feature. - */ - conditions?: FeatureEnablementConditions; - /** - * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object. - */ - variants?: VariantDefinition[]; - /** - * Determines how variants should be allocated for the feature to various users. - */ - allocation?: VariantAllocation; - /** - * The declaration of options used to configure telemetry for this feature. - */ - telemetry?: TelemetryOptions -} - -/** -* The declaration of conditions used to dynamically enable the feature -*/ -interface FeatureEnablementConditions { - /** - * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. - */ - requirement_type?: RequirementType; - /** - * Filters that must run on the client and be evaluated as true for the feature to be considered enabled. - */ - client_filters?: ClientFilter[]; -} - -export type RequirementType = "Any" | "All"; - -interface ClientFilter { - /** - * The name used to refer to a client filter. - */ - name: string; - /** - * Parameters for a given client filter. A client filter can require any set of parameters of any type. - */ - parameters?: Record; -} - -export interface VariantDefinition { - /** - * The name used to refer to a feature variant. - */ - name: string; - /** - * The configuration value for this feature variant. - */ - configuration_value?: unknown; - /** - * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None. - */ - status_override?: "None" | "Enabled" | "Disabled"; -} - -/** -* Determines how variants should be allocated for the feature to various users. -*/ -interface VariantAllocation { - /** - * Specifies which variant should be used when the feature is considered disabled. - */ - default_when_disabled?: string; - /** - * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable. - */ - default_when_enabled?: string; - /** - * A list of objects, each containing a variant name and list of users for whom that variant should be used. - */ - user?: UserAllocation[]; - /** - * A list of objects, each containing a variant name and list of groups for which that variant should be used. - */ - group?: GroupAllocation[]; - /** - * A list of objects, each containing a variant name and percentage range for which that variant should be used. - */ - percentile?: PercentileAllocation[] - /** - * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used. - */ - seed?: string; -} - -interface UserAllocation { - /** - * The name of the variant to use if the user allocation matches the current user. - */ - variant: string; - /** - * Collection of users where if any match the current user, the variant specified in the user allocation is used. - */ - users: string[]; -} - -interface GroupAllocation { - /** - * The name of the variant to use if the group allocation matches a group the current user is in. - */ - variant: string; - /** - * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used. - */ - groups: string[]; -} - -interface PercentileAllocation { - /** - * The name of the variant to use if the calculated percentile for the current user falls in the provided range. - */ - variant: string; - /** - * The lower end of the percentage range for which this variant will be used. - */ - from: number; - /** - * The upper end of the percentage range for which this variant will be used. - */ - to: number; -} - -/** -* The declaration of options used to configure telemetry for this feature. -*/ -interface TelemetryOptions { - /** - * Indicates if telemetry is enabled. - */ - enabled?: boolean; - /** - * A container for metadata that should be bundled with flag telemetry. - */ - metadata?: Record; -} - -// Feature Management Section fed into feature manager. -// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json - -export const FEATURE_MANAGEMENT_KEY = "feature_management"; -export const FEATURE_FLAGS_KEY = "feature_flags"; - -export interface FeatureManagementConfiguration { - feature_management: FeatureManagement -} - -/** - * Declares feature management configuration. - */ -export interface FeatureManagement { - feature_flags: FeatureFlag[]; -} diff --git a/sdk/feature-management/src/schema/model.ts b/sdk/feature-management/src/schema/model.ts new file mode 100644 index 0000000..21749a0 --- /dev/null +++ b/sdk/feature-management/src/schema/model.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Converted from: +// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json + +/** + * A feature flag is a named property that can be toggled to enable or disable some feature of an application. + */ +export interface FeatureFlag { + /** + * An ID used to uniquely identify and reference the feature. + */ + id: string; + /** + * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied. + */ + enabled?: boolean; + /** + * The declaration of conditions used to dynamically enable the feature. + */ + conditions?: FeatureEnablementConditions; + /** + * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object. + */ + variants?: VariantDefinition[]; + /** + * Determines how variants should be allocated for the feature to various users. + */ + allocation?: VariantAllocation; + /** + * The declaration of options used to configure telemetry for this feature. + */ + telemetry?: TelemetryOptions + } + + /** + * The declaration of conditions used to dynamically enable the feature + */ + interface FeatureEnablementConditions { + /** + * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled. + */ + requirement_type?: RequirementType; + /** + * Filters that must run on the client and be evaluated as true for the feature to be considered enabled. + */ + client_filters?: ClientFilter[]; + } + + export type RequirementType = "Any" | "All"; + + interface ClientFilter { + /** + * The name used to refer to a client filter. + */ + name: string; + /** + * Parameters for a given client filter. A client filter can require any set of parameters of any type. + */ + parameters?: Record; + } + + export interface VariantDefinition { + /** + * The name used to refer to a feature variant. + */ + name: string; + /** + * The configuration value for this feature variant. + */ + configuration_value?: unknown; + /** + * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None. + */ + status_override?: "None" | "Enabled" | "Disabled"; + } + + /** + * Determines how variants should be allocated for the feature to various users. + */ + interface VariantAllocation { + /** + * Specifies which variant should be used when the feature is considered disabled. + */ + default_when_disabled?: string; + /** + * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable. + */ + default_when_enabled?: string; + /** + * A list of objects, each containing a variant name and list of users for whom that variant should be used. + */ + user?: UserAllocation[]; + /** + * A list of objects, each containing a variant name and list of groups for which that variant should be used. + */ + group?: GroupAllocation[]; + /** + * A list of objects, each containing a variant name and percentage range for which that variant should be used. + */ + percentile?: PercentileAllocation[] + /** + * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used. + */ + seed?: string; + } + + interface UserAllocation { + /** + * The name of the variant to use if the user allocation matches the current user. + */ + variant: string; + /** + * Collection of users where if any match the current user, the variant specified in the user allocation is used. + */ + users: string[]; + } + + interface GroupAllocation { + /** + * The name of the variant to use if the group allocation matches a group the current user is in. + */ + variant: string; + /** + * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used. + */ + groups: string[]; + } + + interface PercentileAllocation { + /** + * The name of the variant to use if the calculated percentile for the current user falls in the provided range. + */ + variant: string; + /** + * The lower end of the percentage range for which this variant will be used. + */ + from: number; + /** + * The upper end of the percentage range for which this variant will be used. + */ + to: number; + } + + /** + * The declaration of options used to configure telemetry for this feature. + */ + interface TelemetryOptions { + /** + * Indicates if telemetry is enabled. + */ + enabled?: boolean; + /** + * A container for metadata that should be bundled with flag telemetry. + */ + metadata?: Record; + } + + // Feature Management Section fed into feature manager. + // Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json + + export const FEATURE_MANAGEMENT_KEY = "feature_management"; + export const FEATURE_FLAGS_KEY = "feature_flags"; + + export interface FeatureManagementConfiguration { + feature_management: FeatureManagement + } + + /** + * Declares feature management configuration. + */ + export interface FeatureManagement { + feature_flags: FeatureFlag[]; + } diff --git a/sdk/feature-management/src/schema/validator.ts b/sdk/feature-management/src/schema/validator.ts new file mode 100644 index 0000000..97bf5ed --- /dev/null +++ b/sdk/feature-management/src/schema/validator.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Validates a feature flag object, checking if it conforms to the schema. + * @param featureFlag The feature flag object to validate. + */ +export function validateFeatureFlag(featureFlag: any): void { + if (featureFlag === undefined) { + return; // no-op if feature flag is undefined, indicating that the feature flag is not found + } + if (featureFlag === null || typeof featureFlag !== "object") { // Note: typeof null = "object" + throw new TypeError("Feature flag must be an object."); + } + if (typeof featureFlag.id !== "string") { + throw new TypeError("Feature flag 'id' must be a string."); + } + if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { + throw new TypeError("Feature flag 'enabled' must be a boolean."); + } + if (featureFlag.conditions !== undefined) { + validateFeatureEnablementConditions(featureFlag.conditions); + } + if (featureFlag.variants !== undefined) { + validateVariants(featureFlag.variants); + } + if (featureFlag.allocation !== undefined) { + validateVariantAllocation(featureFlag.allocation); + } + if (featureFlag.telemetry !== undefined) { + validateTelemetryOptions(featureFlag.telemetry); + } +} + +function validateFeatureEnablementConditions(conditions: any) { + if (typeof conditions !== "object") { + throw new TypeError("Feature flag 'conditions' must be an object."); + } + if (conditions.requirement_type !== undefined && conditions.requirement_type !== "Any" && conditions.requirement_type !== "All") { + throw new TypeError("'requirement_type' must be 'Any' or 'All'."); + } + if (conditions.client_filters !== undefined) { + validateClientFilters(conditions.client_filters); + } +} + +function validateClientFilters(client_filters: any) { + if (!Array.isArray(client_filters)) { + throw new TypeError("Feature flag conditions 'client_filters' must be an array."); + } + + for (const filter of client_filters) { + if (typeof filter.name !== "string") { + throw new TypeError("Client filter 'name' must be a string."); + } + if (filter.parameters !== undefined && typeof filter.parameters !== "object") { + throw new TypeError("Client filter 'parameters' must be an object."); + } + } +} + +function validateVariants(variants: any) { + if (!Array.isArray(variants)) { + throw new TypeError("Feature flag 'variants' must be an array."); + } + + for (const variant of variants) { + if (typeof variant.name !== "string") { + throw new TypeError("Variant 'name' must be a string."); + } + // skip configuration_value validation as it accepts any type + if (variant.status_override !== undefined && typeof variant.status_override !== "string") { + throw new TypeError("Variant 'status_override' must be a string."); + } + if (variant.status_override !== undefined && variant.status_override !== "None" && variant.status_override !== "Enabled" && variant.status_override !== "Disabled") { + throw new TypeError("Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'."); + } + } +} + +function validateVariantAllocation(allocation: any) { + if (typeof allocation !== "object") { + throw new TypeError("Variant 'allocation' must be an object."); + } + + if (allocation.default_when_disabled !== undefined && typeof allocation.default_when_disabled !== "string") { + throw new TypeError("Variant allocation 'default_when_disabled' must be a string."); + } + if (allocation.default_when_enabled !== undefined && typeof allocation.default_when_enabled !== "string") { + throw new TypeError("Variant allocation 'default_when_enabled' must be a string."); + } + if (allocation.user !== undefined) { + validateUserVariantAllocation(allocation.user); + } + if (allocation.group !== undefined) { + validateGroupVariantAllocation(allocation.group); + } + if (allocation.percentile !== undefined) { + validatePercentileVariantAllocation(allocation.percentile); + } + if (allocation.seed !== undefined && typeof allocation.seed !== "string") { + throw new TypeError("Variant allocation 'seed' must be a string."); + } +} + +function validateUserVariantAllocation(UserAllocations: any) { + if (!Array.isArray(UserAllocations)) { + throw new TypeError("Variant 'user' allocation must be an array."); + } + + for (const allocation of UserAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'user' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("User allocation 'variant' must be a string."); + } + if (!Array.isArray(allocation.users)) { + throw new TypeError("User allocation 'users' must be an array."); + } + for (const user of allocation.users) { + if (typeof user !== "string") { + throw new TypeError("Elements in user allocation 'users' must be strings."); + } + } + } +} + +function validateGroupVariantAllocation(groupAllocations: any) { + if (!Array.isArray(groupAllocations)) { + throw new TypeError("Variant 'group' allocation must be an array."); + } + + for (const allocation of groupAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'group' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("Group allocation 'variant' must be a string."); + } + if (!Array.isArray(allocation.groups)) { + throw new TypeError("Group allocation 'groups' must be an array."); + } + for (const group of allocation.groups) { + if (typeof group !== "string") { + throw new TypeError("Elements in group allocation 'groups' must be strings."); + } + } + } +} + +function validatePercentileVariantAllocation(percentileAllocations: any) { + if (!Array.isArray(percentileAllocations)) { + throw new TypeError("Variant 'percentile' allocation must be an array."); + } + + for (const allocation of percentileAllocations) { + if (typeof allocation !== "object") { + throw new TypeError("Elements in variant 'percentile' allocation must be an object."); + } + if (typeof allocation.variant !== "string") { + throw new TypeError("Percentile allocation 'variant' must be a string."); + } + if (typeof allocation.from !== "number" || allocation.from < 0 || allocation.from > 100) { + throw new TypeError("Percentile allocation 'from' must be a number between 0 and 100."); + } + if (typeof allocation.to !== "number" || allocation.to < 0 || allocation.to > 100) { + throw new TypeError("Percentile allocation 'to' must be a number between 0 and 100."); + } + } +} +// #endregion + +// #region Telemetry +function validateTelemetryOptions(telemetry: any) { + if (typeof telemetry !== "object") { + throw new TypeError("Feature flag 'telemetry' must be an object."); + } + if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") { + throw new TypeError("Telemetry 'enabled' must be a boolean."); + } + if (telemetry.metadata !== undefined && typeof telemetry.metadata !== "object") { + throw new TypeError("Telemetry 'metadata' must be an object."); + } +} +// #endregion diff --git a/sdk/feature-management/test/featureManager.test.ts b/sdk/feature-management/test/featureManager.test.ts index 74723bc..37243a9 100644 --- a/sdk/feature-management/test/featureManager.test.ts +++ b/sdk/feature-management/test/featureManager.test.ts @@ -3,10 +3,11 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../"; chai.use(chaiAsPromised); const expect = chai.expect; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../"; + describe("feature manager", () => { it("should load from json string", () => { const jsonObject = { @@ -71,6 +72,73 @@ describe("feature manager", () => { ]); }); + it("should evaluate features with conditions", () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { + "id": "Gamma", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "invalid type", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": { "Audience": { "DefaultRolloutPercentage": 50 } } } + ] + } + }, + { + "id": "Delta", + "description": "", + "enabled": true, + "conditions": { + "requirement_type": "Any", + "client_filters": [ + { "name": "Microsoft.Targeting", "parameters": "invalid parameter" } + ] + } + } + ], + }); + + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider); + return Promise.all([ + expect(featureManager.isEnabled("Gamma")).eventually.rejectedWith("'requirement_type' must be 'Any' or 'All'."), + expect(featureManager.isEnabled("Delta")).eventually.rejectedWith("Client filter 'parameters' must be an object.") + ]); + }); + + it("should let the last feature flag win", () => { + const jsonObject = { + "feature_management": { + "feature_flags": [ + { "id": "Alpha", "description": "", "enabled": false, "conditions": { "client_filters": [] } }, + { "id": "Alpha", "description": "", "enabled": true, "conditions": { "client_filters": [] } } + ] + } + }; + + const provider1 = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager1 = new FeatureManager(provider1); + + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { "id": "Alpha", "description": "", "enabled": false, "conditions": { "client_filters": [] } }, + { "id": "Alpha", "description": "", "enabled": true, "conditions": { "client_filters": [] } } + ], + }); + + const provider2 = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager2 = new FeatureManager(provider2); + + return Promise.all([ + expect(featureManager1.isEnabled("Alpha")).eventually.eq(true), + expect(featureManager2.isEnabled("Alpha")).eventually.eq(true) + ]); + }); + it("should evaluate features with conditions"); it("should override default filters with custom filters"); }); diff --git a/sdk/feature-management/test/noFilters.test.ts b/sdk/feature-management/test/noFilters.test.ts index e87f04e..efba770 100644 --- a/sdk/feature-management/test/noFilters.test.ts +++ b/sdk/feature-management/test/noFilters.test.ts @@ -3,10 +3,11 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; -import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; chai.use(chaiAsPromised); const expect = chai.expect; +import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; + const featureFlagsDataObject = { "feature_management": { "feature_flags": [ @@ -60,7 +61,7 @@ describe("feature flags with no filters", () => { return Promise.all([ expect(featureManager.isEnabled("BooleanTrue")).eventually.eq(true), expect(featureManager.isEnabled("BooleanFalse")).eventually.eq(false), - expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag InvalidEnabled has an invalid 'enabled' value."), + expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag 'enabled' must be a boolean."), expect(featureManager.isEnabled("Minimal")).eventually.eq(true), expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false), expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true) diff --git a/sdk/feature-management/test/sampleFeatureFlags.ts b/sdk/feature-management/test/sampleFeatureFlags.ts index 9f95b36..85f69b0 100644 --- a/sdk/feature-management/test/sampleFeatureFlags.ts +++ b/sdk/feature-management/test/sampleFeatureFlags.ts @@ -219,7 +219,7 @@ export const featureFlagsConfigurationObject = { "to": 50 } ], - "seed": 1234 + "seed": "1234" }, "telemetry": { "enabled": true @@ -241,7 +241,7 @@ export const featureFlagsConfigurationObject = { "to": 50 } ], - "seed": 12345 + "seed": "12345" }, "telemetry": { "enabled": true @@ -263,7 +263,7 @@ export const featureFlagsConfigurationObject = { "to": 100 } ], - "seed": 12345 + "seed": "12345" }, "telemetry": { "enabled": true