@@ -1241,3 +1241,148 @@ func TestAzureProvider_GetRecommendationsClientForAccount(t *testing.T) {
12411241 assert .Contains (t , err .Error (), "azure provider is not configured" )
12421242 })
12431243}
1244+
1245+ // countingSubscriptionsClient wraps mockSubscriptionsClient and counts how
1246+ // many times NewListPager is invoked, so cache-hit tests can assert the
1247+ // underlying ARM API is only called once.
1248+ type countingSubscriptionsClient struct {
1249+ * mockSubscriptionsClient
1250+ calls int
1251+ }
1252+
1253+ func (c * countingSubscriptionsClient ) NewListPager (options * armsubscriptions.ClientListOptions ) SubscriptionsPager {
1254+ c .calls ++
1255+ return c .mockSubscriptionsClient .NewListPager (options )
1256+ }
1257+
1258+ // twoSubscriptionPages returns a mockSubscriptionsClient listing the same
1259+ // two fixed subscriptions ("sub-1"/"sub-2") every test in this file needs;
1260+ // none of the cache/fan-out tests care about the actual subscription
1261+ // identifiers, so a fixed pair keeps call sites short.
1262+ func twoSubscriptionPages () * mockSubscriptionsClient {
1263+ sub1ID , sub1Name := "sub-1" , "Subscription 1"
1264+ sub2ID , sub2Name := "sub-2" , "Subscription 2"
1265+ return & mockSubscriptionsClient {
1266+ listPagerFunc : func (options * armsubscriptions.ClientListOptions ) SubscriptionsPager {
1267+ return & mockSubscriptionsPager {
1268+ pages : []armsubscriptions.ClientListResponse {
1269+ {
1270+ SubscriptionListResult : armsubscriptions.SubscriptionListResult {
1271+ Value : []* armsubscriptions.Subscription {
1272+ {SubscriptionID : & sub1ID , DisplayName : & sub1Name },
1273+ {SubscriptionID : & sub2ID , DisplayName : & sub2Name },
1274+ },
1275+ },
1276+ },
1277+ },
1278+ }
1279+ },
1280+ }
1281+ }
1282+
1283+ func TestAzureProvider_GetAccounts_CacheHit (t * testing.T ) {
1284+ counting := & countingSubscriptionsClient {mockSubscriptionsClient : twoSubscriptionPages ()}
1285+
1286+ p := & AzureProvider {cred : & mockTokenCredential {}}
1287+ p .SetSubscriptionsClient (counting )
1288+
1289+ first , err := p .GetAccounts (context .Background ())
1290+ require .NoError (t , err )
1291+ require .Len (t , first , 2 )
1292+ assert .Equal (t , 1 , counting .calls , "first GetAccounts call should hit the API once" )
1293+
1294+ second , err := p .GetAccounts (context .Background ())
1295+ require .NoError (t , err )
1296+ require .Len (t , second , 2 )
1297+ assert .Equal (t , 1 , counting .calls , "second GetAccounts call should be served from cache, not the API" )
1298+ assert .Equal (t , first , second )
1299+ }
1300+
1301+ func TestAzureProvider_GetAccounts_CacheHit_ReturnsIndependentCopies (t * testing.T ) {
1302+ p := & AzureProvider {cred : & mockTokenCredential {}}
1303+ p .SetSubscriptionsClient (twoSubscriptionPages ())
1304+
1305+ first , err := p .GetAccounts (context .Background ())
1306+ require .NoError (t , err )
1307+ first [0 ].IsDefault = true // mutate the caller's copy
1308+
1309+ second , err := p .GetAccounts (context .Background ())
1310+ require .NoError (t , err )
1311+ assert .False (t , second [0 ].IsDefault , "mutating a returned slice must not corrupt the cache" )
1312+ }
1313+
1314+ func TestAzureProvider_InvalidateAccountsCache (t * testing.T ) {
1315+ counting := & countingSubscriptionsClient {mockSubscriptionsClient : twoSubscriptionPages ()}
1316+
1317+ p := & AzureProvider {cred : & mockTokenCredential {}}
1318+ p .SetSubscriptionsClient (counting )
1319+
1320+ _ , err := p .GetAccounts (context .Background ())
1321+ require .NoError (t , err )
1322+ assert .Equal (t , 1 , counting .calls )
1323+
1324+ p .InvalidateAccountsCache ()
1325+
1326+ _ , err = p .GetAccounts (context .Background ())
1327+ require .NoError (t , err )
1328+ assert .Equal (t , 2 , counting .calls , "GetAccounts after InvalidateAccountsCache should re-hit the API" )
1329+ }
1330+
1331+ func TestAzureProvider_GetRecommendationsClient_MultiSubscriptionFanOut (t * testing.T ) {
1332+ t .Run ("multi-subscription returns MultiSubscriptionRecommendationsClient" , func (t * testing.T ) {
1333+ p := & AzureProvider {cred : & mockTokenCredential {}}
1334+ p .SetSubscriptionsClient (twoSubscriptionPages ())
1335+
1336+ client , err := p .GetRecommendationsClient (context .Background ())
1337+ require .NoError (t , err )
1338+ require .IsType (t , & MultiSubscriptionRecommendationsClient {}, client )
1339+ assert .Len (t , client .(* MultiSubscriptionRecommendationsClient ).subscriptions , 2 )
1340+ })
1341+
1342+ t .Run ("single discovered subscription returns RecommendationsClientAdapter" , func (t * testing.T ) {
1343+ subID , subName := "sub-solo" , "Solo Subscription"
1344+ p := & AzureProvider {cred : & mockTokenCredential {}}
1345+ p .SetSubscriptionsClient (& mockSubscriptionsClient {
1346+ listPagerFunc : func (options * armsubscriptions.ClientListOptions ) SubscriptionsPager {
1347+ return & mockSubscriptionsPager {
1348+ pages : []armsubscriptions.ClientListResponse {
1349+ {SubscriptionListResult : armsubscriptions.SubscriptionListResult {
1350+ Value : []* armsubscriptions.Subscription {{SubscriptionID : & subID , DisplayName : & subName }},
1351+ }},
1352+ },
1353+ }
1354+ },
1355+ })
1356+
1357+ client , err := p .GetRecommendationsClient (context .Background ())
1358+ require .NoError (t , err )
1359+ require .IsType (t , & RecommendationsClientAdapter {}, client )
1360+ assert .Equal (t , subID , client .(* RecommendationsClientAdapter ).subscriptionID )
1361+ })
1362+
1363+ t .Run ("pinned subscription always returns single adapter regardless of discovered count" , func (t * testing.T ) {
1364+ p := & AzureProvider {cred : & mockTokenCredential {}, subscriptionID : "pinned-sub" }
1365+ // Deliberately do not set a subscriptions client: a pinned subscription
1366+ // must never trigger subscription discovery.
1367+ client , err := p .GetRecommendationsClient (context .Background ())
1368+ require .NoError (t , err )
1369+ require .IsType (t , & RecommendationsClientAdapter {}, client )
1370+ assert .Equal (t , "pinned-sub" , client .(* RecommendationsClientAdapter ).subscriptionID )
1371+ })
1372+
1373+ // The zero-subscription "no Azure subscriptions found" case is already
1374+ // covered by TestAzureProvider_GetRecommendationsClient_WithSubscriptionLookup.
1375+
1376+ t .Run ("subscription discovery failure is propagated" , func (t * testing.T ) {
1377+ p := & AzureProvider {cred : & mockTokenCredential {}}
1378+ p .SetSubscriptionsClient (& mockSubscriptionsClient {
1379+ listPagerFunc : func (options * armsubscriptions.ClientListOptions ) SubscriptionsPager {
1380+ return & mockSubscriptionsPager {nextErr : errors .New ("boom" )}
1381+ },
1382+ })
1383+
1384+ _ , err := p .GetRecommendationsClient (context .Background ())
1385+ assert .Error (t , err )
1386+ assert .Contains (t , err .Error (), "failed to resolve Azure subscriptions" )
1387+ })
1388+ }
0 commit comments