@@ -7,7 +7,7 @@ import chaiAsPromised from "chai-as-promised";
77chai . use ( chaiAsPromised ) ;
88const expect = chai . expect ;
99import { load } from "../src/index.js" ;
10- import { sinon , createMockedConnectionString , createMockedTokenCredential , mockAppConfigurationClientListConfigurationSettings , mockSecretClientGetSecret , restoreMocks , createMockedKeyVaultReference , sleepInMs } from "./utils/testHelper.js" ;
10+ import { sinon , createMockedConnectionString , createMockedTokenCredential , mockAppConfigurationClientListConfigurationSettings , mockAppConfigurationClientGetConfigurationSetting , mockSecretClientGetSecret , restoreMocks , createMockedKeyVaultReference , createMockedKeyValue , sleepInMs } from "./utils/testHelper.js" ;
1111import { KeyVaultSecret , SecretClient } from "@azure/keyvault-secrets" ;
1212import { ErrorMessages , KeyVaultReferenceErrorMessages } from "../src/common/errorMessages.js" ;
1313
@@ -144,6 +144,10 @@ describe("key vault reference", function () {
144144
145145describe ( "key vault secret refresh" , function ( ) {
146146
147+ afterEach ( ( ) => {
148+ restoreMocks ( ) ;
149+ } ) ;
150+
147151 beforeEach ( ( ) => {
148152 const data = [
149153 [ "TestKey" , "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName" , "SecretValue" ]
@@ -153,10 +157,6 @@ describe("key vault secret refresh", function () {
153157 mockAppConfigurationClientListConfigurationSettings ( [ kvs ] ) ;
154158 } ) ;
155159
156- afterEach ( ( ) => {
157- restoreMocks ( ) ;
158- } ) ;
159-
160160 it ( "should not allow secret refresh interval less than 1 minute" , async ( ) => {
161161 const connectionString = createMockedConnectionString ( ) ;
162162 const loadWithInvalidSecretRefreshInterval = load ( connectionString , {
@@ -199,4 +199,151 @@ describe("key vault secret refresh", function () {
199199 expect ( settings . get ( "TestKey" ) ) . eq ( "SecretValue - Updated" ) ;
200200 } ) ;
201201} ) ;
202+
203+ describe ( "min secret refresh interval during key-value refresh" , function ( ) {
204+ let getSecretCallCount = 0 ;
205+ let sentinelEtag = "initial-etag" ;
206+
207+ afterEach ( ( ) => {
208+ restoreMocks ( ) ;
209+ getSecretCallCount = 0 ;
210+ } ) ;
211+
212+ /**
213+ * This test verifies the enforcement of the minimum secret refresh interval during key-value refresh.
214+ * When key-value refresh is triggered (by a watched setting change), the provider calls clearCache()
215+ * on the KeyVaultSecretProvider. However, clearCache() only clears the cache if the minimum secret
216+ * refresh interval (60 seconds) has passed. This prevents overwhelming Key Vaults with too many requests.
217+ */
218+ it ( "should not re-fetch secrets when key-value refresh happens within min secret refresh interval" , async ( ) => {
219+ // Setup: key vault reference + sentinel key for watching
220+ const kvWithSentinel = [
221+ createMockedKeyVaultReference ( "TestKey" , "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName" ) ,
222+ createMockedKeyValue ( { key : "sentinel" , value : "initialValue" , etag : sentinelEtag } )
223+ ] ;
224+ mockAppConfigurationClientListConfigurationSettings ( [ kvWithSentinel ] ) ;
225+ mockAppConfigurationClientGetConfigurationSetting ( kvWithSentinel ) ;
226+
227+ // Mock SecretClient with call counting
228+ const client = new SecretClient ( "https://fake-vault-name.vault.azure.net" , createMockedTokenCredential ( ) ) ;
229+ sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
230+ getSecretCallCount ++ ;
231+ return { value : "SecretValue" } as KeyVaultSecret ;
232+ } ) ;
233+
234+ // Load with key-value refresh enabled (watching sentinel)
235+ const settings = await load ( createMockedConnectionString ( ) , {
236+ refreshOptions : {
237+ enabled : true ,
238+ refreshIntervalInMs : 1000 , // 1 second refresh interval for key-values
239+ watchedSettings : [ { key : "sentinel" } ]
240+ } ,
241+ keyVaultOptions : {
242+ secretClients : [ client ]
243+ }
244+ } ) ;
245+
246+ expect ( settings . get ( "TestKey" ) ) . eq ( "SecretValue" ) ;
247+ expect ( getSecretCallCount ) . eq ( 1 ) ; // Initial load fetched the secret
248+
249+ // Simulate sentinel change to trigger key-value refresh
250+ sentinelEtag = "changed-etag-1" ;
251+ const updatedKvs = [
252+ createMockedKeyVaultReference ( "TestKey" , "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName" ) ,
253+ createMockedKeyValue ( { key : "sentinel" , value : "changedValue1" , etag : sentinelEtag } )
254+ ] ;
255+ restoreMocks ( ) ;
256+ mockAppConfigurationClientListConfigurationSettings ( [ updatedKvs ] ) ;
257+ mockAppConfigurationClientGetConfigurationSetting ( updatedKvs ) ;
258+ sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
259+ getSecretCallCount ++ ;
260+ return { value : "SecretValue" } as KeyVaultSecret ;
261+ } ) ;
262+
263+ // Wait for refresh interval and trigger refresh
264+ await sleepInMs ( 1000 + 100 ) ;
265+ await settings . refresh ( ) ;
266+
267+ // Key-value refresh happened, but secret should NOT be re-fetched
268+ // because min secret refresh interval (60s) hasn't passed
269+ expect ( getSecretCallCount ) . eq ( 1 ) ; // Still 1, no additional getSecret call
270+
271+ // Trigger another key-value refresh
272+ sentinelEtag = "changed-etag-2" ;
273+ const updatedKvs2 = [
274+ createMockedKeyVaultReference ( "TestKey" , "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName" ) ,
275+ createMockedKeyValue ( { key : "sentinel" , value : "changedValue2" , etag : sentinelEtag } )
276+ ] ;
277+ restoreMocks ( ) ;
278+ mockAppConfigurationClientListConfigurationSettings ( [ updatedKvs2 ] ) ;
279+ mockAppConfigurationClientGetConfigurationSetting ( updatedKvs2 ) ;
280+ sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
281+ getSecretCallCount ++ ;
282+ return { value : "SecretValue" } as KeyVaultSecret ;
283+ } ) ;
284+
285+ await sleepInMs ( 1000 + 100 ) ;
286+ await settings . refresh ( ) ;
287+
288+ // Still no additional getSecret call due to min interval enforcement
289+ expect ( getSecretCallCount ) . eq ( 1 ) ;
290+ } ) ;
291+
292+ it ( "should re-fetch secrets after min secret refresh interval passes during key-value refresh" , async ( ) => {
293+ // Setup: key vault reference + sentinel key for watching
294+ let currentSentinelValue = "initialValue" ;
295+ sentinelEtag = "initial-etag" ;
296+
297+ const getKvs = ( ) => [
298+ createMockedKeyVaultReference ( "TestKey" , "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName" ) ,
299+ createMockedKeyValue ( { key : "sentinel" , value : currentSentinelValue , etag : sentinelEtag } )
300+ ] ;
301+
302+ mockAppConfigurationClientListConfigurationSettings ( [ getKvs ( ) ] ) ;
303+ mockAppConfigurationClientGetConfigurationSetting ( getKvs ( ) ) ;
304+
305+ // Mock SecretClient with call counting
306+ const client = new SecretClient ( "https://fake-vault-name.vault.azure.net" , createMockedTokenCredential ( ) ) ;
307+ sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
308+ getSecretCallCount ++ ;
309+ return { value : `SecretValue-${ getSecretCallCount } ` } as KeyVaultSecret ;
310+ } ) ;
311+
312+ // Load with key-value refresh enabled
313+ const settings = await load ( createMockedConnectionString ( ) , {
314+ refreshOptions : {
315+ enabled : true ,
316+ refreshIntervalInMs : 1000 ,
317+ watchedSettings : [ { key : "sentinel" } ]
318+ } ,
319+ keyVaultOptions : {
320+ secretClients : [ client ]
321+ }
322+ } ) ;
323+
324+ expect ( settings . get ( "TestKey" ) ) . eq ( "SecretValue-1" ) ;
325+ expect ( getSecretCallCount ) . eq ( 1 ) ;
326+
327+ // Wait for min secret refresh interval (60 seconds) to pass
328+ await sleepInMs ( 60_000 + 100 ) ;
329+
330+ // Now change sentinel to trigger key-value refresh
331+ currentSentinelValue = "changedValue" ;
332+ sentinelEtag = "changed-etag" ;
333+ restoreMocks ( ) ;
334+ mockAppConfigurationClientListConfigurationSettings ( [ getKvs ( ) ] ) ;
335+ mockAppConfigurationClientGetConfigurationSetting ( getKvs ( ) ) ;
336+ sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
337+ getSecretCallCount ++ ;
338+ return { value : `SecretValue-${ getSecretCallCount } ` } as KeyVaultSecret ;
339+ } ) ;
340+
341+ await sleepInMs ( 1000 + 100 ) ; // Wait for kv refresh interval
342+ await settings . refresh ( ) ;
343+
344+ // Now getSecret SHOULD be called again because min interval has passed
345+ expect ( getSecretCallCount ) . eq ( 2 ) ;
346+ expect ( settings . get ( "TestKey" ) ) . eq ( "SecretValue-2" ) ;
347+ } ) ;
348+ } ) ;
202349/* eslint-enable @typescript-eslint/no-unused-expressions */
0 commit comments