@@ -142,6 +142,163 @@ describe("key vault reference", function () {
142142 } ) ;
143143} ) ;
144144
145+ describe ( "key vault reference deduplication" , function ( ) {
146+ afterEach ( ( ) => {
147+ restoreMocks ( ) ;
148+ } ) ;
149+
150+ // 5 settings all referencing the same secret URI (same sourceId).
151+ const sameSecretUri = "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName" ;
152+ function mockDuplicateReferences ( ) {
153+ const kvs = [ "TestKey1" , "TestKey2" , "TestKey3" , "TestKey4" , "TestKey5" ]
154+ . map ( ( key ) => createMockedKeyVaultReference ( key , sameSecretUri ) ) ;
155+ mockAppConfigurationClientListConfigurationSettings ( [ kvs ] ) ;
156+ }
157+
158+ it ( "should resolve duplicate references with a single Key Vault request in parallel mode" , async ( ) => {
159+ mockDuplicateReferences ( ) ;
160+ const client = new SecretClient ( "https://fake-vault-name.vault.azure.net" , createMockedTokenCredential ( ) ) ;
161+ const stub = sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
162+ // Introduce a delay so that all references start before the first one resolves.
163+ await sleepInMs ( 100 ) ;
164+ return { value : "SecretValue" } as KeyVaultSecret ;
165+ } ) ;
166+
167+ const settings = await load ( createMockedConnectionString ( ) , {
168+ keyVaultOptions : {
169+ secretClients : [ client ] ,
170+ parallelSecretResolutionEnabled : true
171+ }
172+ } ) ;
173+
174+ expect ( stub . callCount ) . eq ( 1 ) ;
175+ for ( const key of [ "TestKey1" , "TestKey2" , "TestKey3" , "TestKey4" , "TestKey5" ] ) {
176+ expect ( settings . get ( key ) ) . eq ( "SecretValue" ) ;
177+ }
178+ } ) ;
179+
180+ it ( "should resolve duplicate references with a single Key Vault request in sequential mode" , async ( ) => {
181+ mockDuplicateReferences ( ) ;
182+ const client = new SecretClient ( "https://fake-vault-name.vault.azure.net" , createMockedTokenCredential ( ) ) ;
183+ const stub = sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
184+ return { value : "SecretValue" } as KeyVaultSecret ;
185+ } ) ;
186+
187+ const settings = await load ( createMockedConnectionString ( ) , {
188+ keyVaultOptions : {
189+ secretClients : [ client ]
190+ }
191+ } ) ;
192+
193+ expect ( stub . callCount ) . eq ( 1 ) ;
194+ for ( const key of [ "TestKey1" , "TestKey2" , "TestKey3" , "TestKey4" , "TestKey5" ] ) {
195+ expect ( settings . get ( key ) ) . eq ( "SecretValue" ) ;
196+ }
197+ } ) ;
198+
199+ it ( "should invoke secret resolver only once for duplicate references" , async ( ) => {
200+ mockDuplicateReferences ( ) ;
201+ const resolver = sinon . stub ( ) . callsFake ( async ( ) => {
202+ await sleepInMs ( 100 ) ;
203+ return "ResolvedSecretValue" ;
204+ } ) ;
205+
206+ const settings = await load ( createMockedConnectionString ( ) , {
207+ keyVaultOptions : {
208+ secretResolver : resolver ,
209+ parallelSecretResolutionEnabled : true
210+ }
211+ } ) ;
212+
213+ expect ( resolver . callCount ) . eq ( 1 ) ;
214+ for ( const key of [ "TestKey1" , "TestKey2" , "TestKey3" , "TestKey4" , "TestKey5" ] ) {
215+ expect ( settings . get ( key ) ) . eq ( "ResolvedSecretValue" ) ;
216+ }
217+ } ) ;
218+
219+ it ( "should fetch different versions of the same secret independently" , async ( ) => {
220+ const versionedUri = "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName/741a0fc52610449baffd6e1c55b9d459" ;
221+ const kvs = [
222+ createMockedKeyVaultReference ( "TestKey" , sameSecretUri ) ,
223+ createMockedKeyVaultReference ( "TestKeyVersioned" , versionedUri )
224+ ] ;
225+ mockAppConfigurationClientListConfigurationSettings ( [ kvs ] ) ;
226+ const client = new SecretClient ( "https://fake-vault-name.vault.azure.net" , createMockedTokenCredential ( ) ) ;
227+ const stub = sinon . stub ( client , "getSecret" ) . callsFake ( async ( _name , options ) => {
228+ await sleepInMs ( 100 ) ;
229+ return { value : options ?. version ? "VersionedValue" : "LatestValue" } as KeyVaultSecret ;
230+ } ) ;
231+
232+ const settings = await load ( createMockedConnectionString ( ) , {
233+ keyVaultOptions : {
234+ secretClients : [ client ] ,
235+ parallelSecretResolutionEnabled : true
236+ }
237+ } ) ;
238+
239+ expect ( stub . callCount ) . eq ( 2 ) ;
240+ expect ( settings . get ( "TestKey" ) ) . eq ( "LatestValue" ) ;
241+ expect ( settings . get ( "TestKeyVersioned" ) ) . eq ( "VersionedValue" ) ;
242+ } ) ;
243+
244+ it ( "should not cache failures and retry on a subsequent attempt" , async ( ) => {
245+ mockDuplicateReferences ( ) ;
246+ const client = new SecretClient ( "https://fake-vault-name.vault.azure.net" , createMockedTokenCredential ( ) ) ;
247+ const stub = sinon . stub ( client , "getSecret" ) ;
248+ // The first (deduplicated) request rejects; the retry attempt succeeds.
249+ // If the failure were cached, the retry would never succeed.
250+ stub . onCall ( 0 ) . callsFake ( async ( ) => {
251+ await sleepInMs ( 100 ) ;
252+ throw new Error ( "Key Vault unavailable" ) ;
253+ } ) ;
254+ stub . callsFake ( async ( ) => {
255+ return { value : "SecretValue" } as KeyVaultSecret ;
256+ } ) ;
257+
258+ const settings = await load ( createMockedConnectionString ( ) , {
259+ keyVaultOptions : {
260+ secretClients : [ client ] ,
261+ parallelSecretResolutionEnabled : true
262+ }
263+ } ) ;
264+
265+ // First round: 5 concurrent references deduped to a single failing request.
266+ // Second round (after load retry): a single succeeding request.
267+ expect ( stub . callCount ) . eq ( 2 ) ;
268+ for ( const key of [ "TestKey1" , "TestKey2" , "TestKey3" , "TestKey4" , "TestKey5" ] ) {
269+ expect ( settings . get ( key ) ) . eq ( "SecretValue" ) ;
270+ }
271+ } ) ;
272+
273+ it ( "should re-fetch once per unique secret on each refresh round" , async ( ) => {
274+ mockDuplicateReferences ( ) ;
275+ const client = new SecretClient ( "https://fake-vault-name.vault.azure.net" , createMockedTokenCredential ( ) ) ;
276+ let callCount = 0 ;
277+ sinon . stub ( client , "getSecret" ) . callsFake ( async ( ) => {
278+ callCount ++ ;
279+ await sleepInMs ( 100 ) ;
280+ return { value : `SecretValue-${ callCount } ` } as KeyVaultSecret ;
281+ } ) ;
282+
283+ const settings = await load ( createMockedConnectionString ( ) , {
284+ keyVaultOptions : {
285+ secretClients : [ client ] ,
286+ secretRefreshIntervalInMs : 60_000 ,
287+ parallelSecretResolutionEnabled : true
288+ }
289+ } ) ;
290+ // Initial load resolves duplicates with a single request.
291+ expect ( callCount ) . eq ( 1 ) ;
292+ expect ( settings . get ( "TestKey1" ) ) . eq ( "SecretValue-1" ) ;
293+
294+ // After the secret refresh interval elapses, the refresh round re-fetches once.
295+ await sleepInMs ( 60_000 + 100 ) ;
296+ await settings . refresh ( ) ;
297+ expect ( callCount ) . eq ( 2 ) ;
298+ expect ( settings . get ( "TestKey1" ) ) . eq ( "SecretValue-2" ) ;
299+ } ) ;
300+ } ) ;
301+
145302describe ( "key vault secret refresh" , function ( ) {
146303
147304 beforeEach ( ( ) => {
0 commit comments