Skip to content

Commit 0c8a4f2

Browse files
committed
add tests for pageEtag based refresh
1 parent 1499cbf commit 0c8a4f2

File tree

4 files changed

+116
-55
lines changed

4 files changed

+116
-55
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
351351
*/
352352
async refresh(): Promise<void> {
353353
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled) {
354-
throw new Error("Refresh is not enabled for key-values and feature flags.");
354+
throw new Error("Refresh is not enabled for key-values or feature flags.");
355355
}
356356

357357
const refreshTasks: Promise<boolean>[] = [];
@@ -482,8 +482,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
482482
}
483483

484484
onRefresh(listener: () => any, thisArg?: any): Disposable {
485-
if (!this.#refreshEnabled) {
486-
throw new Error("Refresh is not enabled for key-values.");
485+
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled) {
486+
throw new Error("Refresh is not enabled for key-values or feature flags.");
487487
}
488488

489489
const boundedListener = listener.bind(thisArg);

test/featureFlag.test.ts

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,38 @@ chai.use(chaiAsPromised);
99
const expect = chai.expect;
1010

1111
const sampleVariantValue = JSON.stringify({
12-
"id": "variant",
13-
"description": "",
14-
"enabled": true,
15-
"variants": [
16-
{
17-
"name": "Off",
18-
"configuration_value": false
19-
},
20-
{
21-
"name": "On",
22-
"configuration_value": true
23-
}
24-
],
25-
"allocation": {
26-
"percentile": [
27-
{
28-
"variant": "Off",
29-
"from": 0,
30-
"to": 40
31-
},
32-
{
33-
"variant": "On",
34-
"from": 49,
35-
"to": 100
36-
}
37-
],
38-
"default_when_enabled": "Off",
39-
"default_when_disabled": "Off"
40-
},
41-
"telemetry": {
42-
"enabled": false
43-
}
12+
"id": "variant",
13+
"description": "",
14+
"enabled": true,
15+
"variants": [
16+
{
17+
"name": "Off",
18+
"configuration_value": false
19+
},
20+
{
21+
"name": "On",
22+
"configuration_value": true
23+
}
24+
],
25+
"allocation": {
26+
"percentile": [
27+
{
28+
"variant": "Off",
29+
"from": 0,
30+
"to": 40
31+
},
32+
{
33+
"variant": "On",
34+
"from": 49,
35+
"to": 100
36+
}
37+
],
38+
"default_when_enabled": "Off",
39+
"default_when_disabled": "Off"
40+
},
41+
"telemetry": {
42+
"enabled": false
43+
}
4444
});
4545

4646
const mockedKVs = [{
@@ -51,9 +51,9 @@ const mockedKVs = [{
5151
value: sampleVariantValue,
5252
contentType: "application/vnd.microsoft.appconfig.ff+json;charset=utf-8",
5353
}].map(createMockedKeyValue).concat([
54-
createMockedFeatureFlag("Beta", true),
55-
createMockedFeatureFlag("Alpha_1", true),
56-
createMockedFeatureFlag("Alpha2", false),
54+
createMockedFeatureFlag("Beta", { enabled: true }),
55+
createMockedFeatureFlag("Alpha_1", { enabled: true }),
56+
createMockedFeatureFlag("Alpha_2", { enabled: false }),
5757
]);
5858

5959
describe("feature flags", function () {

test/refresh.test.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function updateSetting(key: string, value: any) {
1818
setting.etag = uuid.v4();
1919
}
2020
}
21+
2122
function addSetting(key: string, value: any) {
2223
mockedKVs.push(createMockedKeyValue({ key, value }));
2324
}
@@ -43,7 +44,7 @@ describe("dynamic refresh", function () {
4344
const connectionString = createMockedConnectionString();
4445
const settings = await load(connectionString);
4546
const refreshCall = settings.refresh();
46-
return expect(refreshCall).eventually.rejectedWith("Refresh is not enabled for key-values and feature flags.");
47+
return expect(refreshCall).eventually.rejectedWith("Refresh is not enabled for key-values or feature flags.");
4748
});
4849

4950
it("should only allow non-empty list of watched settings when refresh is enabled", async () => {
@@ -124,7 +125,7 @@ describe("dynamic refresh", function () {
124125
it("should throw error when calling onRefresh when refresh is not enabled", async () => {
125126
const connectionString = createMockedConnectionString();
126127
const settings = await load(connectionString);
127-
expect(() => settings.onRefresh(() => { })).throws("Refresh is not enabled for key-values.");
128+
expect(() => settings.onRefresh(() => { })).throws("Refresh is not enabled for key-values or feature flags.");
128129
});
129130

130131
it("should only update values after refreshInterval", async () => {
@@ -326,18 +327,19 @@ describe("dynamic refresh feature flags", function () {
326327
this.timeout(10000);
327328

328329
beforeEach(() => {
329-
mockedKVs = [
330-
createMockedFeatureFlag("Beta", { enabled: true })
331-
];
332-
mockAppConfigurationClientListConfigurationSettings(mockedKVs);
333-
mockAppConfigurationClientGetConfigurationSetting(mockedKVs)
334330
});
335331

336332
afterEach(() => {
337333
restoreMocks();
338334
})
339335

340336
it("should refresh feature flags when enabled", async () => {
337+
mockedKVs = [
338+
createMockedFeatureFlag("Beta", { enabled: true })
339+
];
340+
mockAppConfigurationClientListConfigurationSettings(mockedKVs);
341+
mockAppConfigurationClientGetConfigurationSetting(mockedKVs)
342+
341343
const connectionString = createMockedConnectionString();
342344
const settings = await load(connectionString, {
343345
featureFlagOptions: {
@@ -375,4 +377,50 @@ describe("dynamic refresh feature flags", function () {
375377

376378
});
377379

380+
it("should refresh feature flags only on change, based on page etags", async () => {
381+
// mock multiple pages of feature flags
382+
const page1 = [
383+
createMockedFeatureFlag("Alpha_1", { enabled: true }),
384+
createMockedFeatureFlag("Alpha_2", { enabled: true }),
385+
];
386+
const page2 = [
387+
createMockedFeatureFlag("Beta_1", { enabled: true }),
388+
createMockedFeatureFlag("Beta_2", { enabled: true }),
389+
];
390+
mockAppConfigurationClientListConfigurationSettings(page1, page2);
391+
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2]);
392+
393+
const connectionString = createMockedConnectionString();
394+
const settings = await load(connectionString, {
395+
featureFlagOptions: {
396+
enabled: true,
397+
selectors: [{
398+
keyFilter: "*"
399+
}],
400+
refresh: {
401+
enabled: true,
402+
refreshIntervalInMs: 2000 // 2 seconds for quick test.
403+
}
404+
}
405+
});
406+
407+
let refreshSuccessfulCount = 0;
408+
settings.onRefresh(() => {
409+
refreshSuccessfulCount++;
410+
});
411+
412+
await sleepInMs(2 * 1000 + 1);
413+
await settings.refresh();
414+
expect(refreshSuccessfulCount).eq(0); // no change in feature flags, because page etags are the same.
415+
416+
// change feature flag Beta_1 to false
417+
page2[0] = createMockedFeatureFlag("Beta_1", { enabled: false });
418+
restoreMocks();
419+
mockAppConfigurationClientListConfigurationSettings(page1, page2);
420+
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2]);
421+
422+
await sleepInMs(2 * 1000 + 1);
423+
await settings.refresh();
424+
expect(refreshSuccessfulCount).eq(1); // change in feature flags, because page etags are different.
425+
});
378426
});

test/utils/testHelper.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import * as uuid from "uuid";
99
import { RestError } from "@azure/core-rest-pipeline";
1010
import { promisify } from "util";
1111
const sleepInMs = promisify(setTimeout);
12+
import * as crypto from "crypto";
1213

1314
const TEST_CLIENT_ID = "00000000-0000-0000-0000-000000000000";
1415
const TEST_TENANT_ID = "00000000-0000-0000-0000-000000000000";
1516
const TEST_CLIENT_SECRET = "0000000000000000000000000000000000000000";
1617

18+
function _sha256(input) {
19+
return crypto.createHash("sha256").update(input).digest("hex");
20+
}
21+
1722
function _filterKVs(unfilteredKvs: ConfigurationSetting[], listOptions: any) {
1823
const keyFilter = listOptions?.keyFilter ?? "*";
1924
const labelFilter = listOptions?.labelFilter ?? "*";
@@ -45,7 +50,6 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura
4550

4651
sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => {
4752
let kvs = _filterKVs(pages.flat(), listOptions);
48-
4953
const mockIterator: AsyncIterableIterator<any> & { byPage(): AsyncIterableIterator<any> } = {
5054
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
5155
kvs = _filterKVs(pages.flat(), listOptions);
@@ -57,21 +61,30 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura
5761
},
5862
byPage(): AsyncIterableIterator<any> {
5963
let remainingPages;
64+
const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined; // a copy of the original list
6065
return {
6166
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
62-
remainingPages = [ ...pages ];
67+
remainingPages = [...pages];
6368
return this;
6469
},
6570
next() {
6671
const pageItems = remainingPages.shift();
67-
return Promise.resolve({
68-
done: pageItems === undefined,
69-
value: {
70-
items: _filterKVs(pageItems ?? [], listOptions),
71-
eTag: "etag",
72-
_response: { status: 200 } // TODO: 304 if etag matches
73-
}
74-
});
72+
const pageEtag = pageEtags?.shift();
73+
if (pageItems === undefined) {
74+
return Promise.resolve({ done: true, value: undefined });
75+
} else {
76+
const items = _filterKVs(pageItems ?? [], listOptions);
77+
const etag = _sha256(JSON.stringify(items));
78+
const statusCode = pageEtag === etag ? 304 : 200;
79+
return Promise.resolve({
80+
done: false,
81+
value: {
82+
items,
83+
etag,
84+
_response: { status: statusCode }
85+
}
86+
});
87+
}
7588
}
7689
}
7790
}

0 commit comments

Comments
 (0)