Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 4 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"playwright": "^1.55.0"
},
"dependencies": {
"@azure/app-configuration": "^1.10.0",
"@azure/app-configuration": "^1.11.0",
"@azure/core-rest-pipeline": "^1.6.0",
"@azure/identity": "^4.2.1",
"@azure/keyvault-secrets": "^4.7.0",
Expand Down
3 changes: 2 additions & 1 deletion src/appConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/keyVaultKeyValueAdapter
import { RefreshTimer } from "./refresh/refreshTimer.js";
import {
RequestTracingOptions,
checkConfigurationSettingsWithTrace,
getConfigurationSettingWithTrace,
listConfigurationSettingsWithTrace,
getSnapshotWithTrace,
Expand Down Expand Up @@ -766,7 +767,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
listOptions.pageEtags = pageWatchers.map(w => w.etag ?? "") ;
}

const pageIterator = listConfigurationSettingsWithTrace(
const pageIterator = checkConfigurationSettingsWithTrace(
this.#requestTraceOptions,
client,
listOptions
Expand Down
10 changes: 10 additions & 0 deletions src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ConfigurationSettingId,
GetConfigurationSettingOptions,
ListConfigurationSettingsOptions,
CheckConfigurationSettingsOptions,
GetSnapshotOptions,
ListConfigurationSettingsForSnapshotOptions
} from "@azure/app-configuration";
Expand Down Expand Up @@ -69,6 +70,15 @@ export function listConfigurationSettingsWithTrace(
return client.listConfigurationSettings(actualListOptions);
}

export function checkConfigurationSettingsWithTrace(
requestTracingOptions: RequestTracingOptions,
client: AppConfigurationClient,
checkOptions: CheckConfigurationSettingsOptions
) {
const actualCheckOptions = applyRequestTracing(requestTracingOptions, checkOptions);
return client.checkConfigurationSettings(actualCheckOptions);
}

export function getConfigurationSettingWithTrace(
requestTracingOptions: RequestTracingOptions,
client: AppConfigurationClient,
Expand Down
53 changes: 33 additions & 20 deletions test/afd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,30 +134,35 @@ describe("loadFromAzureFrontDoor", function() {
const kv2_updated = createMockedKeyValue({ key: "app.key2", value: "value2-updated" });
const kv3 = createMockedKeyValue({ key: "app.key3", value: "value3" });

const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
const listStub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
const checkStub = sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings" as any);

stub.onCall(0).returns(getCachedIterator([
// Initial load
listStub.onCall(0).returns(getCachedIterator([
{ items: [kv1, kv2], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
]));

stub.onCall(1).returns(getCachedIterator([
// 1st refresh: check (HEAD) detects change, then reload (GET)
checkStub.onCall(0).returns(getCachedIterator([
{ items: [kv1, kv2_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:01Z") } }
]));
stub.onCall(2).returns(getCachedIterator([
listStub.onCall(1).returns(getCachedIterator([
{ items: [kv1, kv2_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:01Z") } }
]));

stub.onCall(3).returns(getCachedIterator([
// 2nd refresh: check (HEAD) detects change, then reload (GET)
checkStub.onCall(1).returns(getCachedIterator([
{ items: [kv1], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
]));
stub.onCall(4).returns(getCachedIterator([
listStub.onCall(2).returns(getCachedIterator([
{ items: [kv1], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
]));

stub.onCall(5).returns(getCachedIterator([
// 3rd refresh: check (HEAD) detects change, then reload (GET)
checkStub.onCall(2).returns(getCachedIterator([
{ items: [kv1, kv3], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:05Z") } }
]));
stub.onCall(6).returns(getCachedIterator([
listStub.onCall(3).returns(getCachedIterator([
{ items: [kv1, kv3], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:05Z") } }
]));

Expand Down Expand Up @@ -192,19 +197,22 @@ describe("loadFromAzureFrontDoor", function() {
const ff = createMockedFeatureFlag("Beta");
const ff_updated = createMockedFeatureFlag("Beta", { enabled: false });

const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
const listStub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
const checkStub = sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings" as any);

stub.onCall(0).returns(getCachedIterator([
// Initial load: onCall(0) = default KV selector, onCall(1) = feature flags
listStub.onCall(0).returns(getCachedIterator([
{ items: [ff], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
]));
stub.onCall(1).returns(getCachedIterator([
listStub.onCall(1).returns(getCachedIterator([
{ items: [ff], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
]));

stub.onCall(2).returns(getCachedIterator([
// 1st refresh: check (HEAD) detects change, then reload (GET)
checkStub.onCall(0).returns(getCachedIterator([
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
]));
stub.onCall(3).returns(getCachedIterator([
listStub.onCall(2).returns(getCachedIterator([
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
]));

Expand Down Expand Up @@ -235,18 +243,23 @@ describe("loadFromAzureFrontDoor", function() {
const kv1_stale = createMockedKeyValue({ key: "app.key1", value: "stale-value" });
const kv1_new = createMockedKeyValue({ key: "app.key1", value: "new-value" });

const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
stub.onCall(0).returns(getCachedIterator([
const listStub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
const checkStub = sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings" as any);

// Initial load
listStub.onCall(0).returns(getCachedIterator([
{ items: [kv1], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:01Z") } }
]));

stub.onCall(1).returns(getCachedIterator([
{ items: [kv1_stale], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } } // stale response, should not trigger refresh
// 1st refresh: check (HEAD) returns stale response, should not trigger refresh
checkStub.onCall(0).returns(getCachedIterator([
{ items: [kv1_stale], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
]));
stub.onCall(2).returns(getCachedIterator([
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } } // new response, should trigger refresh
// 2nd refresh: check (HEAD) detects change, then reload (GET)
checkStub.onCall(1).returns(getCachedIterator([
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } }
]));
stub.onCall(3).returns(getCachedIterator([
listStub.onCall(1).returns(getCachedIterator([
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } }
]));

Expand Down
60 changes: 60 additions & 0 deletions test/utils/testHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,48 @@ function getCachedIterator(pages: Array<{
return iterator as any;
}

function getMockedHeadIterator(pages: ConfigurationSetting[][], listOptions: any) {
const mockIterator: AsyncIterableIterator<any> & { byPage(): AsyncIterableIterator<any> } = {
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
return this;
},
next() {
return Promise.resolve({ done: true, value: undefined });
},
byPage(): AsyncIterableIterator<any> {
let remainingPages;
const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined;
return {
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
remainingPages = [...pages];
return this;
},
async next() {
const pageItems = remainingPages.shift();
const pageEtag = pageEtags?.shift();
if (pageItems === undefined) {
return { done: true, value: undefined };
} else {
const items = _filterKVs(pageItems ?? [], listOptions);
const etag = await _sha256(JSON.stringify(items));
const statusCode = pageEtag === etag ? 304 : 200;
return {
done: false,
value: {
items: [], // HEAD request returns no items
etag,
_response: { status: statusCode }
}
};
}
}
};
}
};

return mockIterator as any;
}

/**
* Mocks the listConfigurationSettings method of AppConfigurationClient to return the provided pages of ConfigurationSetting.
* E.g.
Expand All @@ -167,6 +209,14 @@ function mockAppConfigurationClientListConfigurationSettings(pages: Configuratio
const kvs = _filterKVs(pages.flat(), listOptions);
return getMockedIterator(pages, kvs, listOptions);
});

sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings").callsFake((listOptions) => {
if (customCallback) {
customCallback(listOptions);
}

return getMockedHeadIterator(pages, listOptions);
});
}

function mockAppConfigurationClientLoadBalanceMode(pages: ConfigurationSetting[][], clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
Expand All @@ -175,6 +225,10 @@ function mockAppConfigurationClientLoadBalanceMode(pages: ConfigurationSetting[]
const kvs = _filterKVs(pages.flat(), listOptions);
return getMockedIterator(pages, kvs, listOptions);
});
sinon.stub(clientWrapper.client, "checkConfigurationSettings").callsFake((listOptions) => {
countObject.count += 1;
return getMockedHeadIterator(pages, listOptions);
});
}

function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationClientWrapper[], isFailoverable: boolean, ...pages: ConfigurationSetting[][]) {
Expand All @@ -189,6 +243,9 @@ function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationCli
sinon.stub(fakeStaticClientWrapper.client, "listConfigurationSettings").callsFake(() => {
throw new RestError("Internal Server Error", { statusCode: 500 });
});
sinon.stub(fakeStaticClientWrapper.client, "checkConfigurationSettings").callsFake(() => {
throw new RestError("Internal Server Error", { statusCode: 500 });
});
clients.push(fakeStaticClientWrapper);

if (!isFailoverable) {
Expand All @@ -202,6 +259,9 @@ function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationCli
const kvs = _filterKVs(pages.flat(), listOptions);
return getMockedIterator(pages, kvs, listOptions);
});
sinon.stub(fakeDynamicClientWrapper.client, "checkConfigurationSettings").callsFake((listOptions) => {
return getMockedHeadIterator(pages, listOptions);
});
return clients;
});
}
Expand Down