Skip to content

Commit b373d1e

Browse files
add more testcases
1 parent fe98b43 commit b373d1e

File tree

6 files changed

+251
-6
lines changed

6 files changed

+251
-6
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"dev": "rollup --config --watch",
2626
"lint": "eslint src/ test/",
2727
"fix-lint": "eslint src/ test/ --fix",
28-
"test": "mocha out/test/load.test.{js,cjs,mjs} --parallel"
28+
"test": "mocha out/test/*.test.{js,cjs,mjs} --parallel"
2929
},
3030
"repository": {
3131
"type": "git",

src/AzureAppConfigurationImpl.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
395395
if (isInputError(error)) {
396396
throw error;
397397
}
398+
if (isRestError(error) && !isFailoverableError(error)) {
399+
throw error;
400+
}
398401
if (abortSignal.aborted) {
399402
return;
400403
}

test/featureFlag.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ const mockedKVs = [{
5555
createMockedFeatureFlag("FlagWithTestLabel", { enabled: true }, {label: "Test"}),
5656
createMockedFeatureFlag("Alpha_1", { enabled: true }),
5757
createMockedFeatureFlag("Alpha_2", { enabled: false }),
58+
createMockedFeatureFlag("DevFeatureFlag", { enabled: true }, { tags: { "environment": "dev" } }),
59+
createMockedFeatureFlag("ProdFeatureFlag", { enabled: false }, { tags: { "environment": "prod" } }),
60+
createMockedFeatureFlag("TaggedFeature", { enabled: true }, { tags: { "team": "backend", "priority": "high" } }),
5861
createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag"}),
5962
createMockedFeatureFlag("Telemetry_2", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag", label: "Test"}),
6063
createMockedFeatureFlag("NoPercentileAndSeed", {
@@ -338,6 +341,78 @@ describe("feature flags", function () {
338341
expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_2?label=Test`);
339342
});
340343

344+
it("should load feature flags using tag filters", async () => {
345+
const connectionString = createMockedConnectionString();
346+
347+
// Test filtering by environment=dev tag
348+
const settingsWithDevTag = await load(connectionString, {
349+
featureFlagOptions: {
350+
enabled: true,
351+
selectors: [{
352+
keyFilter: "*",
353+
tagFilters: ["environment=dev"]
354+
}]
355+
}
356+
});
357+
358+
expect(settingsWithDevTag).not.undefined;
359+
expect(settingsWithDevTag.get("feature_management")).not.undefined;
360+
let featureFlags = settingsWithDevTag.get<any>("feature_management").feature_flags;
361+
expect(featureFlags).not.undefined;
362+
expect((featureFlags as []).length).equals(1);
363+
expect(featureFlags[0].id).equals("DevFeatureFlag");
364+
expect(featureFlags[0].enabled).equals(true);
365+
366+
// Test filtering by environment=prod tag
367+
const settingsWithProdTag = await load(connectionString, {
368+
featureFlagOptions: {
369+
enabled: true,
370+
selectors: [{
371+
keyFilter: "*",
372+
tagFilters: ["environment=prod"]
373+
}]
374+
}
375+
});
376+
377+
featureFlags = settingsWithProdTag.get<any>("feature_management").feature_flags;
378+
expect(featureFlags).not.undefined;
379+
expect((featureFlags as []).length).equals(1);
380+
expect(featureFlags[0].id).equals("ProdFeatureFlag");
381+
expect(featureFlags[0].enabled).equals(false);
382+
383+
// Test filtering by multiple tags (team=backend AND priority=high)
384+
const settingsWithMultipleTags = await load(connectionString, {
385+
featureFlagOptions: {
386+
enabled: true,
387+
selectors: [{
388+
keyFilter: "*",
389+
tagFilters: ["team=backend", "priority=high"]
390+
}]
391+
}
392+
});
393+
394+
featureFlags = settingsWithMultipleTags.get<any>("feature_management").feature_flags;
395+
expect(featureFlags).not.undefined;
396+
expect((featureFlags as []).length).equals(1);
397+
expect(featureFlags[0].id).equals("TaggedFeature");
398+
expect(featureFlags[0].enabled).equals(true);
399+
400+
// Test filtering by non-existent tag
401+
const settingsWithNonExistentTag = await load(connectionString, {
402+
featureFlagOptions: {
403+
enabled: true,
404+
selectors: [{
405+
keyFilter: "*",
406+
tagFilters: ["nonexistent=tag"]
407+
}]
408+
}
409+
});
410+
411+
featureFlags = settingsWithNonExistentTag.get<any>("feature_management").feature_flags;
412+
expect(featureFlags).not.undefined;
413+
expect((featureFlags as []).length).equals(0);
414+
});
415+
341416
it("should load feature flags from snapshot", async () => {
342417
const snapshotName = "Test";
343418
mockAppConfigurationClientGetSnapshot(snapshotName, {compositionType: "key"});

test/load.test.ts

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,24 @@ const mockedKVs = [{
8181
tags: {"tag1": "someValue", "tag2": "someValue"}
8282
}, {
8383
key: "keyWithTag1",
84-
value: "someValue",
84+
value: "someValue1",
8585
tags: {"tag1": "someValue"}
8686
}, {
8787
key: "keyWithTag2",
88-
value: "someValue",
88+
value: "someValue2",
8989
tags: {"tag2": "someValue"}
90+
}, {
91+
key: "keyWithNullTag",
92+
value: "valueWithNullTag",
93+
tags: {"nullTag": "\0"}
94+
}, {
95+
key: "keyWithEscapedComma",
96+
value: "valueWithEscapedComma",
97+
tags: {"tag": "value\\,with\\,commas"}
98+
}, {
99+
key: "keyWithEmptyTag",
100+
value: "valueWithEmptyTag",
101+
tags: {"emptyTag": ""}
90102
}
91103
].map(createMockedKeyValue);
92104

@@ -188,7 +200,7 @@ describe("load", function () {
188200
}]
189201
});
190202
expect(loadWithTag1.has("keyWithTag1")).true;
191-
expect(loadWithTag1.get("keyWithTag1")).eq("someValue");
203+
expect(loadWithTag1.get("keyWithTag1")).eq("someValue1");
192204
expect(loadWithTag1.has("keyWithTag2")).false;
193205
expect(loadWithTag1.has("keyWithMultipleTags")).true;
194206
expect(loadWithTag1.get("keyWithMultipleTags")).eq("someValue");
@@ -205,6 +217,53 @@ describe("load", function () {
205217
expect(loadWithMultipleTags.get("keyWithMultipleTags")).eq("someValue");
206218
});
207219

220+
it("should filter by nullTag to load key values with null tag", async () => {
221+
const connectionString = createMockedConnectionString();
222+
const loadWithNullTag = await load(connectionString, {
223+
selectors: [{
224+
keyFilter: "*",
225+
tagFilters: ["nullTag=\0"]
226+
}]
227+
});
228+
229+
// Should include only key values with nullTag=\0
230+
expect(loadWithNullTag.has("keyWithNullTag")).true;
231+
expect(loadWithNullTag.get("keyWithNullTag")).eq("valueWithNullTag");
232+
233+
// Should exclude key values with other tags
234+
expect(loadWithNullTag.has("keyWithEmptyTag")).false;
235+
});
236+
237+
it("should filter by tags with escaped comma characters", async () => {
238+
const connectionString = createMockedConnectionString();
239+
const loadWithEscapedComma = await load(connectionString, {
240+
selectors: [{
241+
keyFilter: "*",
242+
tagFilters: ["tag=value\\,with\\,commas"]
243+
}]
244+
});
245+
246+
expect(loadWithEscapedComma.has("keyWithEscapedComma")).true;
247+
expect(loadWithEscapedComma.get("keyWithEscapedComma")).eq("valueWithEscapedComma");
248+
});
249+
250+
it("should filter by empty tag value to load key values with empty tag", async () => {
251+
const connectionString = createMockedConnectionString();
252+
const loadWithEmptyTag = await load(connectionString, {
253+
selectors: [{
254+
keyFilter: "*",
255+
tagFilters: ["emptyTag="]
256+
}]
257+
});
258+
259+
// Should include key values with emptyTag=""
260+
expect(loadWithEmptyTag.has("keyWithEmptyTag")).true;
261+
expect(loadWithEmptyTag.get("keyWithEmptyTag")).eq("valueWithEmptyTag");
262+
263+
// Should exclude key values with other tags
264+
expect(loadWithEmptyTag.has("keyWithNullTag")).false;
265+
});
266+
208267
it("should also work with other ReadonlyMap APIs", async () => {
209268
const connectionString = createMockedConnectionString();
210269
const settings = await load(connectionString, {
@@ -319,12 +378,40 @@ describe("load", function () {
319378
const loadWithInvalidTagFilter = load(connectionString, {
320379
selectors: [{
321380
keyFilter: "*",
322-
tagFilters: ["testTag"]
381+
tagFilters: ["emptyTag"]
323382
}]
324383
});
325384
return expect(loadWithInvalidTagFilter).to.eventually.rejectedWith("Tag filter must follow the format \"tagName=tagValue\"");
326385
});
327386

387+
it("should throw exception when too many tag filters are provided", async () => {
388+
const connectionString = createMockedConnectionString();
389+
390+
// Create a list with more than the maximum allowed tag filters (assuming max is 5)
391+
const tooManyTagFilters = [
392+
"Environment=Development",
393+
"Team=Backend",
394+
"Priority=High",
395+
"Version=1.0",
396+
"Stage=Testing",
397+
"Region=EastUS" // This should exceed the limit
398+
];
399+
try {
400+
await load(connectionString, {
401+
selectors: [{
402+
keyFilter: "*",
403+
tagFilters: tooManyTagFilters
404+
}]
405+
});
406+
} catch (error) {
407+
expect(error.message).eq("Failed to load.");
408+
expect(error.cause.message).eq("Invalid request parameter 'tags'. Maximum number of tag filters is 5.");
409+
return;
410+
}
411+
// we should never reach here, load should throw an error
412+
throw new Error("Expected load to throw.");
413+
});
414+
328415
it("should override config settings with same key but different label", async () => {
329416
const connectionString = createMockedConnectionString();
330417
const settings = await load(connectionString, {

test/refresh.test.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ describe("dynamic refresh", function () {
3939
mockedKVs = [
4040
{ value: "red", key: "app.settings.fontColor" },
4141
{ value: "40", key: "app.settings.fontSize" },
42-
{ value: "30", key: "app.settings.fontSize", label: "prod" }
42+
{ value: "30", key: "app.settings.fontSize", label: "prod" },
43+
{ value: "someValue", key: "TestTagKey", tags: { "env": "dev" } }
4344
].map(createMockedKeyValue);
4445
mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvCallback);
4546
mockAppConfigurationClientGetConfigurationSetting(mockedKVs, getKvCallback);
@@ -435,6 +436,34 @@ describe("dynamic refresh", function () {
435436
expect(getKvRequestCount).eq(1);
436437
expect(settings.get("app.settings.fontColor")).eq("blue");
437438
});
439+
440+
it("should refresh key values using tag filters", async () => {
441+
const connectionString = createMockedConnectionString();
442+
const settings = await load(connectionString, {
443+
selectors: [{
444+
keyFilter: "*",
445+
tagFilters: ["env=dev"]
446+
}],
447+
refreshOptions: {
448+
enabled: true,
449+
refreshIntervalInMs: 2000
450+
}
451+
});
452+
453+
expect(settings).not.undefined;
454+
455+
// Verify only dev-tagged items are loaded
456+
expect(settings.get("TestTagKey")).eq("someValue");
457+
458+
// Change the dev-tagged key value
459+
updateSetting("TestTagKey", "newValue");
460+
461+
await sleepInMs(2 * 1000 + 1);
462+
await settings.refresh();
463+
464+
// Verify changes are reflected
465+
expect(settings.get("TestTagKey")).eq("newValue");
466+
});
438467
});
439468

440469
describe("dynamic refresh feature flags", function () {
@@ -549,4 +578,50 @@ describe("dynamic refresh feature flags", function () {
549578
expect(getKvRequestCount).eq(0);
550579
expect(refreshSuccessfulCount).eq(1); // change in feature flags, because page etags are different.
551580
});
581+
582+
it("should refresh feature flags using tag filters", async () => {
583+
mockedKVs = [
584+
createMockedFeatureFlag("DevFeature", { enabled: true }, { tags: { "env": "dev" } }),
585+
createMockedFeatureFlag("ProdFeature", { enabled: false }, { tags: { "env": "prod" } })
586+
];
587+
mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvCallback);
588+
mockAppConfigurationClientGetConfigurationSetting(mockedKVs, getKvCallback);
589+
590+
const connectionString = createMockedConnectionString();
591+
const settings = await load(connectionString, {
592+
featureFlagOptions: {
593+
enabled: true,
594+
selectors: [{
595+
keyFilter: "*",
596+
tagFilters: ["env=dev"]
597+
}],
598+
refresh: {
599+
enabled: true,
600+
refreshIntervalInMs: 2000
601+
}
602+
}
603+
});
604+
605+
expect(settings).not.undefined;
606+
607+
const featureManagement = settings.get<any>("feature_management");
608+
expect(featureManagement).not.undefined;
609+
expect(featureManagement.feature_flags).not.undefined;
610+
expect(featureManagement.feature_flags.length).eq(1);
611+
expect(featureManagement.feature_flags[0].id).eq("DevFeature");
612+
expect(featureManagement.feature_flags[0].enabled).eq(true);
613+
614+
// Change the dev-tagged feature flag
615+
updateSetting(".appconfig.featureflag/DevFeature", JSON.stringify({
616+
"id": "DevFeature",
617+
"enabled": false
618+
}));
619+
620+
await sleepInMs(2 * 1000 + 1);
621+
await settings.refresh();
622+
623+
const updatedFeatureManagement = settings.get<any>("feature_management");
624+
expect(updatedFeatureManagement.feature_flags[0].id).eq("DevFeature");
625+
expect(updatedFeatureManagement.feature_flags[0].enabled).eq(false);
626+
});
552627
});

test/utils/testHelper.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ function _filterKVs(unfilteredKvs: ConfigurationSetting[], listOptions: any) {
2727
const keyFilter = listOptions?.keyFilter ?? "*";
2828
const labelFilter = listOptions?.labelFilter ?? "*";
2929
const tagsFilter = listOptions?.tagsFilter ?? [];
30+
31+
if (tagsFilter.length > 5) {
32+
throw new RestError("Invalid request parameter 'tags'. Maximum number of tag filters is 5.", { statusCode: 400 });
33+
}
34+
3035
return unfilteredKvs.filter(kv => {
3136
const keyMatched = keyFilter.endsWith("*") ? kv.key.startsWith(keyFilter.slice(0, -1)) : kv.key === keyFilter;
3237
let labelMatched = false;

0 commit comments

Comments
 (0)