Skip to content

Commit 540ec3a

Browse files
add testcases
1 parent ddc4a50 commit 540ec3a

10 files changed

+111
-9
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
173173
const { secretRefreshIntervalInMs } = options.keyVaultOptions;
174174
if (secretRefreshIntervalInMs !== undefined) {
175175
if (secretRefreshIntervalInMs < MIN_SECRET_REFRESH_INTERVAL_IN_MS) {
176-
throw new RangeError(`The key vault secret refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
176+
throw new RangeError(`The key vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`);
177177
}
178178
this.#secretRefreshEnabled = true;
179179
this.#secretRefreshTimer = new RefreshTimer(secretRefreshIntervalInMs);
@@ -497,7 +497,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
497497
await this.#updateWatchedKeyValuesEtag(loadedSettings);
498498
}
499499

500-
// clear all cached key vault references
500+
// clear all cached key vault reference configuration settings
501501
this.#secretReferences = [];
502502
for (const setting of loadedSettings) {
503503
if (this.#secretRefreshEnabled && isSecretReference(setting)) {
@@ -591,6 +591,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
591591
}
592592

593593
if (needRefresh) {
594+
for (const adapter of this.#adapters) {
595+
await adapter.onChangeDetected();
596+
}
594597
await this.#loadSelectedAndWatchedKeyValues();
595598
}
596599

src/IKeyValueAdapter.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ export interface IKeyValueAdapter {
1313
* This method process the original configuration setting, and returns processed key and value in an array.
1414
*/
1515
processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]>;
16-
}
16+
17+
/**
18+
* This method is called when a change is detected in the configuration setting.
19+
*/
20+
onChangeDetected(setting?: ConfigurationSetting): void;
21+
}

src/JsonKeyValueAdapter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
3333
}
3434
return [setting.key, parsedValue];
3535
}
36+
37+
async onChangeDetected(): Promise<void> {
38+
return;
39+
}
3640
}
3741

3842
// Determine whether a content type string is a valid JSON content type.

src/keyvault/AzureKeyVaultKeyValueAdapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
4040
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage(error.message, setting, secretIdentifier.sourceId));
4141
}
4242
}
43+
44+
async onChangeDetected(): Promise<void> {
45+
this.#keyVaultSecretProvider.clearCache();
46+
return;
47+
}
4348
}
4449

4550
function buildKeyVaultReferenceErrorMessage(message: string, setting: ConfigurationSetting, secretIdentifier?: string ): string {

src/keyvault/AzureKeyVaultSecretProvider.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { KeyVaultOptions } from "./KeyVaultOptions.js";
55
import { RefreshTimer } from "../refresh/RefreshTimer.js";
66
import { getUrlHost } from "../common/utils.js";
77
import { ArgumentError } from "../error.js";
8-
import { SecretClient, KeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
8+
import { SecretClient, KeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
99

1010
export class AzureKeyVaultSecretProvider {
1111
#keyVaultOptions: KeyVaultOptions | undefined;
12+
#refreshTimer: RefreshTimer | undefined;
1213
#secretClients: Map<string, SecretClient>; // map key vault hostname to corresponding secret client
14+
#cachedSecretValue: Map<string, any> = new Map<string, any>(); // map secret identifier to secret value
1315

1416
constructor(keyVaultOptions: KeyVaultOptions | undefined, refreshTimer?: RefreshTimer) {
1517
if (keyVaultOptions?.secretRefreshIntervalInMs !== undefined) {
@@ -21,13 +23,37 @@ export class AzureKeyVaultSecretProvider {
2123
}
2224
}
2325
this.#keyVaultOptions = keyVaultOptions;
26+
this.#refreshTimer = refreshTimer;
2427
this.#secretClients = new Map();
2528
for (const client of this.#keyVaultOptions?.secretClients ?? []) {
2629
this.#secretClients.set(getUrlHost(client.vaultUrl), client);
2730
}
2831
}
2932

3033
async getSecretValue(secretIdentifier: KeyVaultSecretIdentifier): Promise<unknown> {
34+
if (this.#refreshTimer && !this.#refreshTimer.canRefresh()) {
35+
// return the cached secret value if it exists
36+
if (this.#cachedSecretValue.has(secretIdentifier.sourceId)) {
37+
const cachedValue = this.#cachedSecretValue.get(secretIdentifier.sourceId);
38+
return cachedValue;
39+
}
40+
// not found in cache, get the secret value from key vault
41+
const secretValue = await this.#getSecretValueFromKeyVault(secretIdentifier);
42+
this.#cachedSecretValue.set(secretIdentifier.sourceId, secretValue);
43+
return secretValue;
44+
}
45+
46+
// Always reload the secret value from key vault when the refresh timer expires.
47+
const secretValue = await this.#getSecretValueFromKeyVault(secretIdentifier);
48+
this.#cachedSecretValue.set(secretIdentifier.sourceId, secretValue);
49+
return secretValue;
50+
}
51+
52+
clearCache(): void {
53+
this.#cachedSecretValue.clear();
54+
}
55+
56+
async #getSecretValueFromKeyVault(secretIdentifier: KeyVaultSecretIdentifier): Promise<unknown> {
3157
if (!this.#keyVaultOptions) {
3258
throw new ArgumentError("Failed to get secret value. The keyVaultOptions is not configured.");
3359
}
@@ -43,6 +69,7 @@ export class AzureKeyVaultSecretProvider {
4369
}
4470
// When code reaches here, it means that the key vault reference cannot be resolved in all possible ways.
4571
throw new ArgumentError("Failed to get secret value. No key vault secret client, credential or secret resolver callback is available to resolve the secret.");
72+
4673
}
4774

4875
#getSecretClient(vaultUrl: URL): SecretClient | undefined {

src/keyvault/KeyVaultOptions.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export interface KeyVaultOptions {
2424
* Specifies the refresh interval in milliseconds for periodically reloading secret from Key Vault.
2525
* @remarks
2626
* If specified, the value must be greater than 60 seconds.
27-
* Any refresh operation triggered using @see AzureAppConfiguration.refresh() will not update the value for a Key Vault secret until the refresh interval has expired.
2827
*/
2928
secretRefreshIntervalInMs?: number;
3029

test/exportedApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export { load } from "../src";
4+
export { load } from "../src";

test/keyvault.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised";
66
chai.use(chaiAsPromised);
77
const expect = chai.expect;
88
import { load } from "./exportedApi.js";
9-
import { MAX_TIME_OUT, sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } from "./utils/testHelper.js";
9+
import { MAX_TIME_OUT, sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference, sleepInMs } from "./utils/testHelper.js";
1010
import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets";
1111

1212
const mockedData = [
@@ -113,3 +113,62 @@ describe("key vault reference", function () {
113113
expect(settings.get("TestKey2")).eq("SecretValue2");
114114
});
115115
});
116+
117+
describe("key vault secret refresh", function () {
118+
this.timeout(MAX_TIME_OUT);
119+
120+
beforeEach(() => {
121+
const data = [
122+
["TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName", "SecretValue"]
123+
];
124+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
125+
const kvs = data.map(([key, vaultUri, _value]) => createMockedKeyVaultReference(key, vaultUri));
126+
mockAppConfigurationClientListConfigurationSettings([kvs]);
127+
});
128+
129+
afterEach(() => {
130+
restoreMocks();
131+
});
132+
133+
it("should not allow secret refresh interval less than 1 minute", async () => {
134+
const connectionString = createMockedConnectionString();
135+
const loadWithInvalidSecretRefreshInterval = load(connectionString, {
136+
keyVaultOptions: {
137+
secretClients: [
138+
new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential()),
139+
],
140+
secretRefreshIntervalInMs: 59999
141+
}
142+
});
143+
return expect(loadWithInvalidSecretRefreshInterval).eventually.rejectedWith("The key vault secret refresh interval cannot be less than 60000 milliseconds.");
144+
});
145+
146+
it("should reload key vault secret when there is no change to key-values", async () => {
147+
const client = new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential());
148+
const stub = sinon.stub(client, "getSecret");
149+
stub.onCall(0).resolves({ value: "SecretValue" } as KeyVaultSecret);
150+
stub.onCall(1).resolves({ value: "SecretValue - Updated" } as KeyVaultSecret);
151+
152+
const settings = await load(createMockedConnectionString(), {
153+
keyVaultOptions: {
154+
secretClients: [
155+
client
156+
],
157+
credential: createMockedTokenCredential(),
158+
secretRefreshIntervalInMs: 60_000
159+
}
160+
});
161+
expect(settings).not.undefined;
162+
expect(settings.get("TestKey")).eq("SecretValue");
163+
164+
await sleepInMs(30_000);
165+
await settings.refresh();
166+
// use cached value
167+
expect(settings.get("TestKey")).eq("SecretValue");
168+
169+
await sleepInMs(30_000);
170+
await settings.refresh();
171+
// secret refresh interval expires, reload secret value
172+
expect(settings.get("TestKey")).eq("SecretValue - Updated");
173+
});
174+
});

test/refresh.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ describe("dynamic refresh", function () {
438438
});
439439

440440
describe("dynamic refresh feature flags", function () {
441-
this.timeout(10000);
441+
this.timeout(MAX_TIME_OUT);
442442

443443
beforeEach(() => {
444444
});

test/utils/testHelper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as crypto from "crypto";
1313
import { ConfigurationClientManager } from "../../src/ConfigurationClientManager.js";
1414
import { ConfigurationClientWrapper } from "../../src/ConfigurationClientWrapper.js";
1515

16-
const MAX_TIME_OUT = 20000;
16+
const MAX_TIME_OUT = 100_000;
1717

1818
const TEST_CLIENT_ID = "00000000-0000-0000-0000-000000000000";
1919
const TEST_TENANT_ID = "00000000-0000-0000-0000-000000000000";

0 commit comments

Comments
 (0)