Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const featureFlagPrefix = ".appconfig.featureflag/";

// @public
export interface FeatureFlagValue {
conditions: {
conditions?: {
clientFilters: {
name: string;
parameters?: Record<string, unknown>;
Expand All @@ -145,7 +145,7 @@ export interface FeatureFlagValue {
};
description?: string;
displayName?: string;
enabled: boolean;
enabled?: boolean;
id?: string;
}

Expand Down
28 changes: 18 additions & 10 deletions sdk/appconfiguration/app-configuration/src/featureFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface FeatureFlagValue {
*
* [More Info](https://learn.microsoft.com/azure/azure-app-configuration/howto-feature-filters-aspnet-core)
*/
conditions: {
conditions?: {
clientFilters: { name: string; parameters?: Record<string, unknown> }[];
requirementType?: "All" | "Any";
};
Expand All @@ -41,7 +41,7 @@ export interface FeatureFlagValue {
/**
* Boolean flag to say if the feature flag is enabled.
*/
enabled: boolean;
enabled?: boolean;
/**
* Display name for the feature to use for display rather than the ID.
*/
Expand Down Expand Up @@ -69,14 +69,18 @@ export const FeatureFlagHelper = {
}
const jsonFeatureFlagValue: JsonFeatureFlagValue = {
id: featureFlag.value.id ?? key.replace(featureFlagPrefix, ""),
enabled: featureFlag.value.enabled,
enabled: featureFlag.value.enabled ?? false,
description: featureFlag.value.description,
conditions: {
};

if (featureFlag.value.conditions) {
jsonFeatureFlagValue.conditions = {
client_filters: featureFlag.value.conditions.clientFilters,
requirement_type: featureFlag.value.conditions.requirementType ?? "Any",
},
display_name: featureFlag.value.displayName,
};
};
}

jsonFeatureFlagValue.display_name = featureFlag.value.displayName;

const configSetting = {
...featureFlag,
Expand Down Expand Up @@ -114,14 +118,18 @@ export function parseFeatureFlag(
enabled: jsonFeatureFlagValue.enabled,
description: jsonFeatureFlagValue.description,
displayName: jsonFeatureFlagValue.display_name,
conditions: { clientFilters: jsonFeatureFlagValue.conditions.client_filters },
},
key,
contentType: featureFlagContentType,
};

if (jsonFeatureFlagValue.conditions.requirement_type) {
featureflag.value.conditions.requirementType = jsonFeatureFlagValue.conditions.requirement_type;
if (jsonFeatureFlagValue.conditions) {
featureflag.value.conditions = {
clientFilters: jsonFeatureFlagValue.conditions.client_filters,
};
if (jsonFeatureFlagValue.conditions.requirement_type) {
featureflag.value.conditions.requirementType = jsonFeatureFlagValue.conditions.requirement_type;
}
}
return featureflag;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
* @internal
*/
export type JsonFeatureFlagValue = {
conditions: {
conditions?: {
client_filters: { name: string; parameters?: Record<string, unknown> }[];
requirement_type?: "All" | "Any";
};
description?: string;
enabled: boolean;
enabled?: boolean;
id?: string;
display_name?: string;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { ConfigurationSetting } from "../../src/index.js";
import { featureFlagContentType } from "../../src/index.js";
import { parseFeatureFlag } from "../../src/featureFlag.js";
import { describe, it, assert } from "vitest";

describe("FeatureFlag - Optional Fields", () => {
it("should parse feature flag with only id field", () => {
const minimalFeatureFlag = {
key: ".appconfig.featureflag/minimal-flag",
value: JSON.stringify({ id: "minimal-flag" }),
contentType: featureFlagContentType,
} as ConfigurationSetting;

const parsed = parseFeatureFlag(minimalFeatureFlag);
assert.equal(parsed.value.id, "minimal-flag");
assert.isUndefined(parsed.value.enabled);
assert.isUndefined(parsed.value.conditions);
assert.isUndefined(parsed.value.description);
assert.isUndefined(parsed.value.displayName);
});

it("should parse feature flag without conditions", () => {
const featureFlagWithoutConditions = {
key: ".appconfig.featureflag/no-conditions",
value: JSON.stringify({
id: "no-conditions",
enabled: true,
description: "A feature flag without conditions",
display_name: "No Conditions Flag",
}),
contentType: featureFlagContentType,
} as ConfigurationSetting;

const parsed = parseFeatureFlag(featureFlagWithoutConditions);
assert.equal(parsed.value.id, "no-conditions");
assert.equal(parsed.value.enabled, true);
assert.equal(parsed.value.description, "A feature flag without conditions");
assert.equal(parsed.value.displayName, "No Conditions Flag");
assert.isUndefined(parsed.value.conditions);
});

it("should parse feature flag with empty conditions", () => {
const featureFlagWithEmptyConditions = {
key: ".appconfig.featureflag/empty-conditions",
value: JSON.stringify({
id: "empty-conditions",
enabled: false,
conditions: {
client_filters: [],
},
}),
contentType: featureFlagContentType,
} as ConfigurationSetting;

const parsed = parseFeatureFlag(featureFlagWithEmptyConditions);
assert.equal(parsed.value.id, "empty-conditions");
assert.equal(parsed.value.enabled, false);
assert.isDefined(parsed.value.conditions);
assert.deepEqual(parsed.value.conditions?.clientFilters, []);
assert.isUndefined(parsed.value.conditions?.requirementType);
});

it("should parse feature flag with conditions and requirement_type", () => {
const fullFeatureFlag = {
key: ".appconfig.featureflag/full-flag",
value: JSON.stringify({
id: "full-flag",
enabled: true,
description: "Full feature flag",
display_name: "Full Flag",
conditions: {
client_filters: [
{ name: "Filter1", parameters: { key: "value" } },
{ name: "Filter2" },
],
requirement_type: "All",
},
}),
contentType: featureFlagContentType,
} as ConfigurationSetting;

const parsed = parseFeatureFlag(fullFeatureFlag);
assert.equal(parsed.value.id, "full-flag");
assert.equal(parsed.value.enabled, true);
assert.equal(parsed.value.description, "Full feature flag");
assert.equal(parsed.value.displayName, "Full Flag");
assert.isDefined(parsed.value.conditions);
assert.equal(parsed.value.conditions?.clientFilters.length, 2);
assert.equal(parsed.value.conditions?.requirementType, "All");
});
});
Comment on lines 9 to 91
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests only cover deserialization (parsing) of feature flags with optional fields. Consider adding round-trip tests that serialize and then deserialize feature flags with undefined enabled and conditions fields. Since FeatureFlagHelper is internal, you can test the serialization path using addConfigurationSetting followed by getConfigurationSetting. For example:

it("should handle round-trip for feature flag with undefined enabled field", async () => {
  const featureFlag = {
    key: `${featureFlagPrefix}minimal-flag`,
    value: { id: "minimal-flag" },
    contentType: featureFlagContentType,
  } as ConfigurationSetting<FeatureFlagValue>;
  
  await client.addConfigurationSetting(featureFlag);
  const retrieved = await client.getConfigurationSetting({ key: featureFlag.key });
  const parsed = parseFeatureFlag(retrieved);
  
  // Verify the enabled field behavior after round-trip
  assert.isUndefined(parsed.value.enabled); // or assert.equal(parsed.value.enabled, false) depending on intended behavior
});

This would help ensure consistency between serialization and deserialization for optional fields.

Copilot uses AI. Check for mistakes.
Loading