Skip to content

Commit f5031df

Browse files
handle corner case response status is string
1 parent dc98e5c commit f5031df

3 files changed

Lines changed: 132 additions & 16 deletions

File tree

src/appConfigurationImpl.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
659659

660660
const watcher: SettingWatcher = this.#sentinels.get(watchedSetting)!; // watcher should always exist for sentinels
661661
const isDeleted = response === undefined && watcher.etag !== undefined; // previously existed, now deleted
662-
const isChanged = response && response.statusCode === 200 && watcher.etag !== response.etag; // etag changed
662+
const isChanged = response && Number(response.statusCode) === 200 && watcher.etag !== response.etag; // etag changed
663663
if (isDeleted || isChanged) {
664664
changedSentinel = watchedSetting;
665665
changedSentinelWatcher = { etag: isChanged ? response.etag : undefined };
@@ -750,7 +750,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
750750

751751
for await (const page of pageIterator) {
752752
// when conditional request is sent, the response will be 304 if not changed
753-
if (page._response.status === 200) { // created or changed
753+
if (Number(page._response.status) === 200) { // created or changed
754754
return true;
755755
}
756756
}
@@ -779,7 +779,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
779779
try {
780780
response = await this.#executeWithFailoverPolicy(funcToExecute);
781781
} catch (error) {
782-
if (isRestError(error) && error.statusCode === 404) {
782+
if (isRestError(error) && Number(error.statusCode) === 404) {
783783
response = undefined;
784784
} else {
785785
throw error;
@@ -822,7 +822,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
822822
try {
823823
response = await this.#executeWithFailoverPolicy(funcToExecute);
824824
} catch (error) {
825-
if (isRestError(error) && error.statusCode === 404) {
825+
if (isRestError(error) && Number(error.statusCode) === 404) {
826826
response = undefined;
827827
} else {
828828
throw error;

test/refresh.test.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import chaiAsPromised from "chai-as-promised";
77
chai.use(chaiAsPromised);
88
const expect = chai.expect;
99
import { load } from "../src/index.js";
10-
import { mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedFeatureFlag } from "./utils/testHelper.js";
10+
import { mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientListConfigurationSettingsWithStringStatus, mockAppConfigurationClientGetConfigurationSetting, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedFeatureFlag } from "./utils/testHelper.js";
1111
import * as uuid from "uuid";
1212

1313
let mockedKVs: any[] = [];
@@ -513,6 +513,54 @@ describe("dynamic refresh", function () {
513513
// Verify changes are reflected
514514
expect(settings.get("TestTagKey")).eq("newValue");
515515
});
516+
517+
it("should refresh key values when page response status is string", async () => {
518+
restoreMocks(); // restore the mock set up in beforeEach
519+
const initialKVs = [
520+
{ value: "red", key: "app.settings.fontColor" },
521+
{ value: "40", key: "app.settings.fontSize" }
522+
].map(createMockedKeyValue);
523+
mockAppConfigurationClientListConfigurationSettingsWithStringStatus([initialKVs], listKvCallback);
524+
mockAppConfigurationClientGetConfigurationSetting(initialKVs, getKvCallback);
525+
526+
const connectionString = createMockedConnectionString();
527+
const settings = await load(connectionString, {
528+
refreshOptions: {
529+
enabled: true,
530+
refreshIntervalInMs: 2000
531+
}
532+
});
533+
expect(listKvRequestCount).eq(1);
534+
expect(settings).not.undefined;
535+
expect(settings.get("app.settings.fontColor")).eq("red");
536+
537+
let refreshSuccessfulCount = 0;
538+
settings.onRefresh(() => {
539+
refreshSuccessfulCount++;
540+
});
541+
542+
// no change yet, the page response status is "304"
543+
await sleepInMs(2 * 1000 + 1);
544+
await settings.refresh();
545+
expect(listKvRequestCount).eq(2); // one more conditional request to detect change
546+
expect(refreshSuccessfulCount).eq(0); // no change in key values, because page etags are the same.
547+
548+
// change key value
549+
restoreMocks();
550+
const changedKVs = [
551+
{ value: "blue", key: "app.settings.fontColor" },
552+
{ value: "40", key: "app.settings.fontSize" }
553+
].map(createMockedKeyValue);
554+
mockAppConfigurationClientListConfigurationSettingsWithStringStatus([changedKVs], listKvCallback);
555+
mockAppConfigurationClientGetConfigurationSetting(changedKVs, getKvCallback);
556+
557+
// the page response status is "200" when changed
558+
await sleepInMs(2 * 1000 + 1);
559+
await settings.refresh();
560+
expect(listKvRequestCount).eq(4); // 2 + 2 more requests: one conditional request to detect change and one request to reload all key values
561+
expect(refreshSuccessfulCount).eq(1); // change in key values, because page etags are different.
562+
expect(settings.get("app.settings.fontColor")).eq("blue");
563+
});
516564
});
517565

518566
describe("dynamic refresh feature flags", function () {
@@ -672,5 +720,60 @@ describe("dynamic refresh feature flags", function () {
672720
expect(updatedFeatureManagement.feature_flags[0].id).eq("DevFeature");
673721
expect(updatedFeatureManagement.feature_flags[0].enabled).eq(false);
674722
});
723+
724+
it("should refresh feature flags when page response status is string", async () => {
725+
// mock multiple pages of feature flags
726+
const page1 = [
727+
createMockedFeatureFlag("Alpha_1", { enabled: true }),
728+
createMockedFeatureFlag("Alpha_2", { enabled: true }),
729+
];
730+
const page2 = [
731+
createMockedFeatureFlag("Beta_1", { enabled: true }),
732+
createMockedFeatureFlag("Beta_2", { enabled: true }),
733+
];
734+
mockAppConfigurationClientListConfigurationSettingsWithStringStatus([page1, page2], listKvCallback);
735+
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2], getKvCallback);
736+
737+
const connectionString = createMockedConnectionString();
738+
const settings = await load(connectionString, {
739+
featureFlagOptions: {
740+
enabled: true,
741+
selectors: [{
742+
keyFilter: "*"
743+
}],
744+
refresh: {
745+
enabled: true,
746+
refreshIntervalInMs: 2000 // 2 seconds for quick test.
747+
}
748+
}
749+
});
750+
expect(listKvRequestCount).eq(2);
751+
expect(getKvRequestCount).eq(0);
752+
753+
let refreshSuccessfulCount = 0;
754+
settings.onRefresh(() => {
755+
refreshSuccessfulCount++;
756+
});
757+
758+
// no change yet, the page response status is "304"
759+
await sleepInMs(2 * 1000 + 1);
760+
await settings.refresh();
761+
expect(listKvRequestCount).eq(3); // one conditional request to detect change
762+
expect(getKvRequestCount).eq(0);
763+
expect(refreshSuccessfulCount).eq(0); // no change in feature flags, because page etags are the same.
764+
765+
// change feature flag Beta_1 to false
766+
page2[0] = createMockedFeatureFlag("Beta_1", { enabled: false });
767+
restoreMocks();
768+
mockAppConfigurationClientListConfigurationSettingsWithStringStatus([page1, page2], listKvCallback);
769+
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2], getKvCallback);
770+
771+
// the page response status is "200" when changed
772+
await sleepInMs(2 * 1000 + 1);
773+
await settings.refresh();
774+
expect(listKvRequestCount).eq(5); // 3 + 2 more requests: one conditional request to detect change and one request to reload all feature flags
775+
expect(getKvRequestCount).eq(0);
776+
expect(refreshSuccessfulCount).eq(1); // change in feature flags, because page etags are different.
777+
});
675778
});
676779
/* eslint-enable @typescript-eslint/no-unused-expressions */

test/utils/testHelper.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { AppConfigurationClient, ConfigurationSetting, featureFlagContentType, s
66
import { ClientSecretCredential } from "@azure/identity";
77
import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets";
88
import * as uuid from "uuid";
9-
import { RestError } from "@azure/core-rest-pipeline";
9+
import { RestError, PipelineRequest, PipelineResponse, SendRequest } from "@azure/core-rest-pipeline";
1010
import { ConfigurationClientManager } from "../../src/configurationClientManager.js";
1111
import { ConfigurationClientWrapper } from "../../src/configurationClientWrapper.js";
1212

@@ -51,7 +51,7 @@ function _filterKVs(unfilteredKvs: ConfigurationSetting[], listOptions: any) {
5151
}
5252
let tagsMatched = true;
5353
if (tagsFilter.length > 0) {
54-
tagsMatched = tagsFilter.every(tag => {
54+
tagsMatched = tagsFilter.every((tag: string) => {
5555
const [tagName, tagValue] = tag.split("=");
5656
if (tagValue === "\0") {
5757
return kv.tags && kv.tags[tagName] === null;
@@ -63,7 +63,7 @@ function _filterKVs(unfilteredKvs: ConfigurationSetting[], listOptions: any) {
6363
});
6464
}
6565

66-
function getMockedIterator(pages: ConfigurationSetting[][], kvs: ConfigurationSetting[], listOptions: any) {
66+
function getMockedIterator(pages: ConfigurationSetting[][], kvs: ConfigurationSetting[], listOptions: any, useStringStatus: boolean = false) {
6767
const mockIterator: AsyncIterableIterator<any> & { byPage(): AsyncIterableIterator<any> } = {
6868
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
6969
kvs = _filterKVs(pages.flat(), listOptions);
@@ -74,7 +74,7 @@ function getMockedIterator(pages: ConfigurationSetting[][], kvs: ConfigurationSe
7474
return Promise.resolve({ done: !value, value });
7575
},
7676
byPage(): AsyncIterableIterator<any> {
77-
let remainingPages;
77+
let remainingPages: ConfigurationSetting[][];
7878
const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined; // a copy of the original list
7979
return {
8080
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
@@ -95,7 +95,7 @@ function getMockedIterator(pages: ConfigurationSetting[][], kvs: ConfigurationSe
9595
value: {
9696
items,
9797
etag,
98-
_response: { status: statusCode }
98+
_response: { status: useStringStatus ? `${statusCode}` : statusCode }
9999
}
100100
};
101101
}
@@ -114,7 +114,7 @@ function getMockedIterator(pages: ConfigurationSetting[][], kvs: ConfigurationSe
114114
*
115115
* @param pages List of pages, each page is a list of ConfigurationSetting
116116
*/
117-
function mockAppConfigurationClientListConfigurationSettings(pages: ConfigurationSetting[][], customCallback?: (listOptions) => any) {
117+
function mockAppConfigurationClientListConfigurationSettings(pages: ConfigurationSetting[][], customCallback?: (listOptions: any) => any) {
118118

119119
sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => {
120120
if (customCallback) {
@@ -126,6 +126,18 @@ function mockAppConfigurationClientListConfigurationSettings(pages: Configuratio
126126
});
127127
}
128128

129+
function mockAppConfigurationClientListConfigurationSettingsWithStringStatus(pages: ConfigurationSetting[][], customCallback?: (listOptions: any) => any) {
130+
131+
sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => {
132+
if (customCallback) {
133+
customCallback(listOptions);
134+
}
135+
136+
const kvs = _filterKVs(pages.flat(), listOptions);
137+
return getMockedIterator(pages, kvs, listOptions, true);
138+
});
139+
}
140+
129141
function mockAppConfigurationClientLoadBalanceMode(pages: ConfigurationSetting[][], clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
130142
sinon.stub(clientWrapper.client, "listConfigurationSettings").callsFake((listOptions) => {
131143
countObject.count += 1;
@@ -163,7 +175,7 @@ function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationCli
163175
});
164176
}
165177

166-
function mockAppConfigurationClientGetConfigurationSetting(kvList: any[], customCallback?: (options) => any) {
178+
function mockAppConfigurationClientGetConfigurationSetting(kvList: any[], customCallback?: (options: any) => any) {
167179
sinon.stub(AppConfigurationClient.prototype, "getConfigurationSetting").callsFake((settingId, options) => {
168180
if (customCallback) {
169181
customCallback(options);
@@ -182,7 +194,7 @@ function mockAppConfigurationClientGetConfigurationSetting(kvList: any[], custom
182194
});
183195
}
184196

185-
function mockAppConfigurationClientGetSnapshot(snapshotResponses: Map<string, any>, customCallback?: (options) => any) {
197+
function mockAppConfigurationClientGetSnapshot(snapshotResponses: Map<string, any>, customCallback?: (options: any) => any) {
186198
sinon.stub(AppConfigurationClient.prototype, "getSnapshot").callsFake((name, options) => {
187199
if (customCallback) {
188200
customCallback(options);
@@ -196,7 +208,7 @@ function mockAppConfigurationClientGetSnapshot(snapshotResponses: Map<string, an
196208
});
197209
}
198210

199-
function mockAppConfigurationClientListConfigurationSettingsForSnapshot(snapshotResponses: Map<string, ConfigurationSetting[][]>, customCallback?: (options) => any) {
211+
function mockAppConfigurationClientListConfigurationSettingsForSnapshot(snapshotResponses: Map<string, ConfigurationSetting[][]>, customCallback?: (options: any) => any) {
200212
sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettingsForSnapshot").callsFake((name, listOptions) => {
201213
if (customCallback) {
202214
customCallback(listOptions);
@@ -218,7 +230,7 @@ function mockSecretClientGetSecret(uriValueList: [string, string][]) {
218230
dict.set(uri, value);
219231
}
220232

221-
sinon.stub(SecretClient.prototype, "getSecret").callsFake(async function (secretName, options) {
233+
sinon.stub(SecretClient.prototype, "getSecret").callsFake(async function (this: SecretClient, secretName, options) {
222234
const url = new URL(this.vaultUrl);
223235
url.pathname = `/secrets/${secretName}`;
224236
if (options?.version) {
@@ -314,7 +326,7 @@ class HttpRequestHeadersPolicy {
314326
this.headers = {};
315327
this.name = "HttpRequestHeadersPolicy";
316328
}
317-
sendRequest(req, next) {
329+
sendRequest(req: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
318330
this.headers = req.headers;
319331
return next(req).then(resp => resp);
320332
}
@@ -323,6 +335,7 @@ class HttpRequestHeadersPolicy {
323335
export {
324336
sinon,
325337
mockAppConfigurationClientListConfigurationSettings,
338+
mockAppConfigurationClientListConfigurationSettingsWithStringStatus,
326339
mockAppConfigurationClientGetConfigurationSetting,
327340
mockAppConfigurationClientGetSnapshot,
328341
mockAppConfigurationClientListConfigurationSettingsForSnapshot,

0 commit comments

Comments
 (0)