From 69a96b1dce814e970bb890e0821d2ed253e34b81 Mon Sep 17 00:00:00 2001 From: msbrett Date: Wed, 3 Jun 2026 10:10:13 -0700 Subject: [PATCH 01/10] fix(finops-hub): add NAT Gateway to private subnets (#2161) Private-mode FinOps Hub deployments fail under the Azure built-in policy "Subnets should be private" (fe505f54d90b47b3b60de101), which denies any subnet whose 'defaultOutboundAccess' is not explicitly set to false. This becomes the platform default after the September 2025 retirement of implicit subnet outbound (https://aka.ms/defaultoutboundaccess). Naively setting defaultOutboundAccess:false breaks the hub because: - script-subnet hosts ACI containers (deploymentScripts image azuredeploymentscripts-powershell) that need outbound to mcr.microsoft.com. - dataExplorer-subnet hosts the ADX cluster which pulls open-data CSVs from raw.githubusercontent.com via externaldata() KQL. Per https://learn.microsoft.com/azure/container-instances/container-instances-virtual-network-concepts the supported outbound pattern for ACI-in-VNet is a Standard NAT Gateway with a Standard static Public IP. Changes (private mode only; no public-mode impact): - Set defaultOutboundAccess:false on all 3 subnets. - Add Standard NAT Gateway + Standard static Public IP. - Attach NAT Gateway to script-subnet and dataExplorer-subnet. (private-endpoint-subnet is inbound-only for PEs and does not need NAT.) Verified end-to-end in MCAPS: - Private mode: pr2163-min-priv-1780505343 Succeeded (RG pr2163-uat-min-private) - Public mode: pr2163-min-pub-1780505424 Succeeded (RG pr2163-uat-min-public) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Core/infrastructure.bicep | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep index 0cf23a808..8ce238a8f 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep @@ -17,6 +17,8 @@ param hub HubProperties //============================================================================== var nsgName = '${hub.routing.networkName}-nsg' +var natGatewayName = '${hub.routing.networkName}-natgw' +var natGatewayPipName = '${hub.routing.networkName}-natgw-pip' // Workaround https://github.com/Azure/bicep/issues/1853 var finopsHubSubnetName = 'private-endpoint-subnet' @@ -28,6 +30,7 @@ var subnets = !hub.options.privateRouting ? [] : [ name: finopsHubSubnetName properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 28, 0) + defaultOutboundAccess: false networkSecurityGroup: { id: nsg.id } @@ -42,6 +45,10 @@ var subnets = !hub.options.privateRouting ? [] : [ name: scriptSubnetName properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 28, 1) + defaultOutboundAccess: false + natGateway: { + id: resourceId('Microsoft.Network/natGateways', natGatewayName) + } networkSecurityGroup: { id: nsg.id } @@ -64,6 +71,10 @@ var subnets = !hub.options.privateRouting ? [] : [ name: dataExplorerSubnetName properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 27, 1) + defaultOutboundAccess: false + natGateway: { + id: resourceId('Microsoft.Network/natGateways', natGatewayName) + } networkSecurityGroup: { id: nsg.id } @@ -172,6 +183,9 @@ resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.p name: hub.routing.networkName location: hub.location tags: getHubTags(hub, 'Microsoft.Storage/virtualNetworks') + dependsOn: [ + natGateway + ] properties: { addressSpace: { addressPrefixes: [hub.options.networkAddressPrefix] @@ -192,6 +206,42 @@ resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.p } } +//------------------------------------------------------------------------------ +// NAT Gateway (provides outbound for script-subnet + dataExplorer-subnet when +// defaultOutboundAccess is disabled; required by the 'Subnets should be private' +// policy and the September 2025 implicit-outbound retirement) +//------------------------------------------------------------------------------ + +resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (hub.options.privateRouting) { + name: natGatewayPipName + location: hub.location + tags: getHubTags(hub, 'Microsoft.Storage/publicIPAddresses') + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + } +} + +resource natGateway 'Microsoft.Network/natGateways@2023-11-01' = if (hub.options.privateRouting) { + name: natGatewayName + location: hub.location + tags: getHubTags(hub, 'Microsoft.Storage/natGateways') + sku: { + name: 'Standard' + } + properties: { + idleTimeoutInMinutes: 4 + publicIpAddresses: [ + { + id: natGatewayPublicIp.id + } + ] + } +} + //------------------------------------------------------------------------------ // Storage DNS zones //------------------------------------------------------------------------------ From affa9423221be51e815f151eb736196a8d8a4496 Mon Sep 17 00:00:00 2001 From: msbrett Date: Wed, 3 Jun 2026 14:13:42 -0700 Subject: [PATCH 02/10] Make NAT Gateway opt-in via enableNatGateway parameter Adds an opt-in NAT Gateway for the FinOps Hub's private-mode subnets. When enablePublicAccess is false and enableNatGateway is true, the script and dataExplorer subnets attach to a NAT Gateway and all three subnets set defaultOutboundAccess=false. When enableNatGateway is false (default), subnets keep Azure default outbound access and no NAT resources deploy. Surfaces: - createUiDefinition.json: CheckBox in Networking step + output mapping - main.bicep / hub.bicep / fx/hub-types.bicep: enableNatGateway parameter threaded through newHub() into HubOptions.natGateway - Microsoft.FinOpsHubs/Core/infrastructure.bicep: gates NAT resources and subnet defaultOutboundAccess on hub.options.natGateway - Deploy-FinOpsHub.ps1: -EnableNatGateway switch passed as ARM parameter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/powershell/Public/Deploy-FinOpsHub.ps1 | 8 ++++++++ .../finops-hub/createUiDefinition.json | 9 +++++++++ src/templates/finops-hub/main.bicep | 4 ++++ .../Core/infrastructure.bicep | 18 +++++++++--------- .../finops-hub/modules/fx/hub-types.bicep | 6 ++++++ src/templates/finops-hub/modules/hub.bicep | 4 ++++ 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/powershell/Public/Deploy-FinOpsHub.ps1 b/src/powershell/Public/Deploy-FinOpsHub.ps1 index 26d9713d2..4f5f3ccd6 100644 --- a/src/powershell/Public/Deploy-FinOpsHub.ps1 +++ b/src/powershell/Public/Deploy-FinOpsHub.ps1 @@ -87,6 +87,9 @@ .PARAMETER EnablePublicAccess Optional. Enable public access to the data lake. Default: true. + .PARAMETER EnableNatGateway + Optional. Deploy a NAT Gateway for controlled outbound access when private routing is enabled (DisablePublicAccess). When set, subnets disable Azure default outbound access and route through the NAT Gateway. Ignored when DisablePublicAccess is not set. Default: false. + .PARAMETER VirtualNetworkAddressPrefix Optional. Address space for the workload. A /26 is required for the workload. Default: "10.20.30.0/26". @@ -191,6 +194,10 @@ function Deploy-FinOpsHub [switch] $DisablePublicAccess, + [Parameter()] + [switch] + $EnableNatGateway, + [Parameter()] [string] $VirtualNetworkAddressPrefix = '10.20.30.0/26', @@ -282,6 +289,7 @@ function Deploy-FinOpsHub $parameterSplat.TemplateParameterObject.Add('dataExplorerRawRetentionInDays', $DataExplorerRawRetentionInDays) $parameterSplat.TemplateParameterObject.Add('dataExplorerFinalRetentionInMonths', $DataExplorerFinalRetentionInMonths) $parameterSplat.TemplateParameterObject.Add('enablePublicAccess', -not $DisablePublicAccess) + $parameterSplat.TemplateParameterObject.Add('enableNatGateway', $EnableNatGateway.IsPresent) $parameterSplat.TemplateParameterObject.Add('virtualNetworkAddressPrefix', $VirtualNetworkAddressPrefix) $parameterSplat.TemplateParameterObject.Add('exportRetentionInDays', $ExportRetentionInDays) $parameterSplat.TemplateParameterObject.Add('ingestionRetentionInMonths', $IngestionRetentionInMonths) diff --git a/src/templates/finops-hub/createUiDefinition.json b/src/templates/finops-hub/createUiDefinition.json index 726974856..a07611180 100644 --- a/src/templates/finops-hub/createUiDefinition.json +++ b/src/templates/finops-hub/createUiDefinition.json @@ -950,6 +950,14 @@ ] }, "visible": "[equals(steps('advanced').networking.enablePublicAccess, false)]" + }, + { + "name": "enableNatGateway", + "type": "Microsoft.Common.CheckBox", + "label": "Deploy NAT Gateway and disable default outbound access", + "toolTip": "Required by the September 2025 'Subnets should be private' policy. Adds a Standard NAT Gateway and Standard static Public IP attached to the script and Data Explorer subnets to provide controlled outbound internet access. Leave unchecked if you have your own egress path (UDR to firewall, peered hub with NAT, etc.).", + "defaultValue": false, + "visible": "[equals(steps('advanced').networking.enablePublicAccess, false)]" } ], "visible": true @@ -993,6 +1001,7 @@ "enableAHBRecommendations": "[steps('recommendations').optional.enableAHBRecommendations]", "enableSpotRecommendations": "[steps('recommendations').optional.enableSpotRecommendations]", "enablePublicAccess": "[steps('advanced').networking.enablePublicAccess]", + "enableNatGateway": "[steps('advanced').networking.enableNatGateway]", "virtualNetworkAddressPrefix": "[steps('advanced').networking.virtualNetworkAddressPrefix]", "dataExplorerSku": "[steps('pricing').dataExplorer.dataExplorerSku]", "exportRetentionInDays": "[steps('retention').storage.msexportsDays]", diff --git a/src/templates/finops-hub/main.bicep b/src/templates/finops-hub/main.bicep index 4e058fcba..687c26174 100644 --- a/src/templates/finops-hub/main.bicep +++ b/src/templates/finops-hub/main.bicep @@ -159,6 +159,9 @@ param dataExplorerFinalRetentionInMonths int = 13 @description('Optional. Enable public access to FinOps hubs resources. Default: true.') param enablePublicAccess bool = true +@description('Optional. Deploy a NAT Gateway for controlled outbound access when private routing is enabled. When true, subnets disable Azure default outbound access and route through the NAT Gateway. Ignored when enablePublicAccess is true. Default: false.') +param enableNatGateway bool = false + @description('Optional. Address space for the workload. Minimum /26 subnet size is required for the workload. Default: "10.20.30.0/26".') param virtualNetworkAddressPrefix string = '10.20.30.0/26' @@ -195,6 +198,7 @@ module hub 'modules/hub.bicep' = { remoteHubStorageUri: remoteHubStorageUri remoteHubStorageKey: remoteHubStorageKey enablePublicAccess: enablePublicAccess + enableNatGateway: enableNatGateway virtualNetworkAddressPrefix: virtualNetworkAddressPrefix } } diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep index 8ce238a8f..020609acc 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep @@ -30,7 +30,7 @@ var subnets = !hub.options.privateRouting ? [] : [ name: finopsHubSubnetName properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 28, 0) - defaultOutboundAccess: false + defaultOutboundAccess: !hub.options.natGateway networkSecurityGroup: { id: nsg.id } @@ -45,10 +45,10 @@ var subnets = !hub.options.privateRouting ? [] : [ name: scriptSubnetName properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 28, 1) - defaultOutboundAccess: false - natGateway: { + defaultOutboundAccess: !hub.options.natGateway + natGateway: hub.options.natGateway ? { id: resourceId('Microsoft.Network/natGateways', natGatewayName) - } + } : null networkSecurityGroup: { id: nsg.id } @@ -71,10 +71,10 @@ var subnets = !hub.options.privateRouting ? [] : [ name: dataExplorerSubnetName properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 27, 1) - defaultOutboundAccess: false - natGateway: { + defaultOutboundAccess: !hub.options.natGateway + natGateway: hub.options.natGateway ? { id: resourceId('Microsoft.Network/natGateways', natGatewayName) - } + } : null networkSecurityGroup: { id: nsg.id } @@ -212,7 +212,7 @@ resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.p // policy and the September 2025 implicit-outbound retirement) //------------------------------------------------------------------------------ -resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (hub.options.privateRouting) { +resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (hub.options.natGateway) { name: natGatewayPipName location: hub.location tags: getHubTags(hub, 'Microsoft.Storage/publicIPAddresses') @@ -225,7 +225,7 @@ resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = i } } -resource natGateway 'Microsoft.Network/natGateways@2023-11-01' = if (hub.options.privateRouting) { +resource natGateway 'Microsoft.Network/natGateways@2023-11-01' = if (hub.options.natGateway) { name: natGatewayName location: hub.location tags: getHubTags(hub, 'Microsoft.Storage/natGateways') diff --git a/src/templates/finops-hub/modules/fx/hub-types.bicep b/src/templates/finops-hub/modules/fx/hub-types.bicep index 2cb8e050e..d9b197e75 100644 --- a/src/templates/finops-hub/modules/fx/hub-types.bicep +++ b/src/templates/finops-hub/modules/fx/hub-types.bicep @@ -72,6 +72,7 @@ type HubRoutingProperties = { keyVaultSku: 'KeyVault SKU. Allowed values: "standard", "premium".' keyVaultEnablePurgeProtection: 'Indicates whether purge protection is enabled for the Key Vault. When enabled, deleted Key Vault and its secrets cannot be permanently deleted until the retention period expires, which is required for compliance in some environments.' networkAddressPrefix: 'Address prefix for the FinOps hub isolated virtual network, if private network routing is enabled.' + natGateway: 'Indicates whether a NAT Gateway should be deployed for controlled outbound internet access. When enabled, subnets disable Azure default outbound access and route through the NAT Gateway.' privateRouting: 'Indicates whether private network routing is enabled.' publisherIsolation: 'Indicates whether FinOps hub resources should be separated by publisher for advanced security.' storageInfrastructureEncryption: 'Indicates whether infrastructure encryption is enabled for the storage account.' @@ -96,6 +97,7 @@ type HubProperties = { keyVaultSku: string keyVaultEnablePurgeProtection: bool networkAddressPrefix: string + natGateway: bool privateRouting: bool publisherIsolation: bool storageInfrastructureEncryption: bool @@ -192,6 +194,7 @@ func newHubInternal( keyVaultEnablePurgeProtection bool, enableInfrastructureEncryption bool, enablePublicAccess bool, + enableNatGateway bool, networkName string, networkAddressPrefix string, isTelemetryEnabled bool, @@ -211,6 +214,7 @@ func newHubInternal( keyVaultSku: keyVaultSku keyVaultEnablePurgeProtection: keyVaultEnablePurgeProtection networkAddressPrefix: networkAddressPrefix + natGateway: !enablePublicAccess && enableNatGateway privateRouting: !enablePublicAccess publisherIsolation: false // TODO: Expose publisher isolation option storageInfrastructureEncryption: enableInfrastructureEncryption @@ -251,6 +255,7 @@ func newHub( keyVaultEnablePurgeProtection bool, enableInfrastructureEncryption bool, enablePublicAccess bool, + enableNatGateway bool, networkAddressPrefix string, isTelemetryEnabled bool, ) HubProperties => newHubInternal( @@ -265,6 +270,7 @@ func newHub( keyVaultEnablePurgeProtection, enableInfrastructureEncryption, enablePublicAccess, + enableNatGateway, '${safeStorageName(name)}-vnet-${location}', // networkName, cSpell:ignore vnet networkAddressPrefix, isTelemetryEnabled ?? true diff --git a/src/templates/finops-hub/modules/hub.bicep b/src/templates/finops-hub/modules/hub.bicep index 2b5603372..0c9e6b9e5 100644 --- a/src/templates/finops-hub/modules/hub.bicep +++ b/src/templates/finops-hub/modules/hub.bicep @@ -170,6 +170,9 @@ param dataExplorerFinalRetentionInMonths int = 13 @description('Optional. Enable public access to the data lake. Default: true.') param enablePublicAccess bool = true +@description('Optional. Deploy a NAT Gateway for controlled outbound access when private routing is enabled. When true, subnets disable Azure default outbound access and route through the NAT Gateway. Ignored when enablePublicAccess is true. Default: false.') +param enableNatGateway bool = false + @description('Optional. Address space for the workload. Minimum /26 subnet size is required for the workload. Default: "10.20.30.0/26".') param virtualNetworkAddressPrefix string = '10.20.30.0/26' @@ -194,6 +197,7 @@ var hub = newHub( enablePurgeProtection, enableInfrastructureEncryption, enablePublicAccess, + enableNatGateway, virtualNetworkAddressPrefix, enableDefaultTelemetry ) From 79c783206067a242685a4d53084402d1a0af2c5c Mon Sep 17 00:00:00 2001 From: msbrett Date: Wed, 3 Jun 2026 15:13:38 -0700 Subject: [PATCH 03/10] Add -NetworkMode to Deploy-FinOpsHub and Deploy-Hub scripts Addresses Roland's review feedback: surface the three network modes through a single enum on the PowerShell deployment scripts so CLI/module users can reach 'vnet' and 'private' modes without juggling two switches. Deploy-FinOpsHub.ps1: - Add -NetworkMode with ValidateSet 'public','vnet','private' - Translate to ARM params: enablePublicAccess, enableNatGateway - Deprecate -DisablePublicAccess (kept for back-compat, warns and maps to 'vnet' when -NetworkMode is not supplied) - Drop unreleased -EnableNatGateway switch (folded into -NetworkMode) - Fix stale .PARAMETER EnablePublicAccess help block referencing a parameter the cmdlet doesn't expose Deploy-Hub.ps1: - Add -NetworkMode with same ValidateSet - -Private and -VirtualNetworkAddressPrefix imply 'vnet' (back-compat) - 'private' mode opts in to the NAT Gateway + locked-down subnets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/powershell/Public/Deploy-FinOpsHub.ps1 | 22 +++++++++++++--------- src/scripts/Deploy-Hub.ps1 | 16 +++++++++++----- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/powershell/Public/Deploy-FinOpsHub.ps1 b/src/powershell/Public/Deploy-FinOpsHub.ps1 index 4f5f3ccd6..e5af58495 100644 --- a/src/powershell/Public/Deploy-FinOpsHub.ps1 +++ b/src/powershell/Public/Deploy-FinOpsHub.ps1 @@ -84,11 +84,11 @@ .PARAMETER DataExplorerFinalRetentionInMonths Optional. Number of months of data to retain in the Data Explorer *_final_v* tables. Default: 13. - .PARAMETER EnablePublicAccess - Optional. Enable public access to the data lake. Default: true. + .PARAMETER NetworkMode + Optional. Network mode for the hub: 'public' (default), 'vnet' (private endpoints, default outbound), or 'private' (private endpoints + NAT Gateway, subnets locked down with defaultOutboundAccess=false - required when the 'Subnets should be private' policy is enforced). - .PARAMETER EnableNatGateway - Optional. Deploy a NAT Gateway for controlled outbound access when private routing is enabled (DisablePublicAccess). When set, subnets disable Azure default outbound access and route through the NAT Gateway. Ignored when DisablePublicAccess is not set. Default: false. + .PARAMETER DisablePublicAccess + Optional. Deprecated. Use -NetworkMode 'vnet' or -NetworkMode 'private' instead. When set without -NetworkMode, behaves as -NetworkMode 'vnet'. Ignored when -NetworkMode is supplied. .PARAMETER VirtualNetworkAddressPrefix Optional. Address space for the workload. A /26 is required for the workload. Default: "10.20.30.0/26". @@ -191,12 +191,13 @@ function Deploy-FinOpsHub $DataExplorerFinalRetentionInMonths = 13, [Parameter()] - [switch] - $DisablePublicAccess, + [ValidateSet('public', 'vnet', 'private')] + [string] + $NetworkMode, [Parameter()] [switch] - $EnableNatGateway, + $DisablePublicAccess, [Parameter()] [string] @@ -288,8 +289,11 @@ function Deploy-FinOpsHub $parameterSplat.TemplateParameterObject.Add('dataExplorerCapacity', $DataExplorerCapacity) $parameterSplat.TemplateParameterObject.Add('dataExplorerRawRetentionInDays', $DataExplorerRawRetentionInDays) $parameterSplat.TemplateParameterObject.Add('dataExplorerFinalRetentionInMonths', $DataExplorerFinalRetentionInMonths) - $parameterSplat.TemplateParameterObject.Add('enablePublicAccess', -not $DisablePublicAccess) - $parameterSplat.TemplateParameterObject.Add('enableNatGateway', $EnableNatGateway.IsPresent) + # Resolve network mode. -NetworkMode wins; -DisablePublicAccess (deprecated) maps to 'vnet'. + $effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' } + if (-not $NetworkMode -and $DisablePublicAccess) { Write-Warning "-DisablePublicAccess is deprecated; use -NetworkMode 'vnet' or -NetworkMode 'private'." } + $parameterSplat.TemplateParameterObject.Add('enablePublicAccess', ($effectiveNetworkMode -eq 'public')) + $parameterSplat.TemplateParameterObject.Add('enableNatGateway', ($effectiveNetworkMode -eq 'private')) $parameterSplat.TemplateParameterObject.Add('virtualNetworkAddressPrefix', $VirtualNetworkAddressPrefix) $parameterSplat.TemplateParameterObject.Add('exportRetentionInDays', $ExportRetentionInDays) $parameterSplat.TemplateParameterObject.Add('ingestionRetentionInMonths', $IngestionRetentionInMonths) diff --git a/src/scripts/Deploy-Hub.ps1 b/src/scripts/Deploy-Hub.ps1 index b48779470..3cc008d9a 100644 --- a/src/scripts/Deploy-Hub.ps1 +++ b/src/scripts/Deploy-Hub.ps1 @@ -89,7 +89,10 @@ Optional. Use managed exports instead of manual exports. Requires -Scope. Grants the hub managed identity the required roles on the scope and passes scopesToMonitor to the template. .PARAMETER Private - Optional. Deploy with private networking (VNet and private endpoints). Default: false. + Optional. Deploy with private networking (VNet and private endpoints, default outbound access). Equivalent to -NetworkMode vnet. Kept for back-compat. Default: false. + + .PARAMETER NetworkMode + Optional. Network mode: 'public' (default), 'vnet' (private endpoints, default outbound), or 'private' (private endpoints + NAT Gateway, subnets locked down — required when the 'Subnets should be private' policy is enforced). .PARAMETER VirtualNetworkAddressPrefix Optional. Virtual network address prefix for private networking. Requires a /26 CIDR block. When set, also sets -Private. Default: "10.20.30.0/26". @@ -117,6 +120,8 @@ param( [string]$Scope, [switch]$ManagedExports, [switch]$Private, + [ValidateSet('public', 'vnet', 'private')] + [string]$NetworkMode, [string]$VirtualNetworkAddressPrefix, [string]$Location, [switch]$Build, @@ -283,16 +288,17 @@ else $params.enableManagedExports = $false } -# Private networking (infer from VNet prefix if provided) -if ($VirtualNetworkAddressPrefix) { $Private = $true } -if ($Private) +# Network mode — -NetworkMode wins; -Private (back-compat) and -VirtualNetworkAddressPrefix imply 'vnet'. +if (-not $NetworkMode -and ($Private -or $VirtualNetworkAddressPrefix)) { $NetworkMode = 'vnet' } +if ($NetworkMode -and $NetworkMode -ne 'public') { $params.enablePublicAccess = $false + $params.enableNatGateway = ($NetworkMode -eq 'private') if ($VirtualNetworkAddressPrefix) { $params.virtualNetworkAddressPrefix = $VirtualNetworkAddressPrefix } - Write-Host " Private networking: enabled (VNet $($VirtualNetworkAddressPrefix ?? '10.20.30.0/26'))" + Write-Host " Network mode: $NetworkMode (VNet $($VirtualNetworkAddressPrefix ?? '10.20.30.0/26'))" } # Resource group From b12c7ddf6f5603b273c1d02af3bcdbfc26505426 Mon Sep 17 00:00:00 2001 From: msbrett Date: Wed, 3 Jun 2026 16:35:25 -0700 Subject: [PATCH 04/10] Address Copilot review: conditional dependsOn + NAT/PIP tag namespaces - infrastructure.bicep: gate vNet dependsOn on hub.options.natGateway so it doesn't reference a non-deployed resource when NAT is disabled - infrastructure.bicep: fix tag namespaces on the two NAT resources introduced in this PR (publicIPAddresses and natGateways had Microsoft.Storage/* keys; corrected to Microsoft.Network/*) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.FinOpsHubs/Core/infrastructure.bicep | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep index 020609acc..f269cf9eb 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep @@ -183,9 +183,9 @@ resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.p name: hub.routing.networkName location: hub.location tags: getHubTags(hub, 'Microsoft.Storage/virtualNetworks') - dependsOn: [ + dependsOn: hub.options.natGateway ? [ natGateway - ] + ] : [] properties: { addressSpace: { addressPrefixes: [hub.options.networkAddressPrefix] @@ -215,7 +215,7 @@ resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.p resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = if (hub.options.natGateway) { name: natGatewayPipName location: hub.location - tags: getHubTags(hub, 'Microsoft.Storage/publicIPAddresses') + tags: getHubTags(hub, 'Microsoft.Network/publicIPAddresses') sku: { name: 'Standard' } @@ -228,7 +228,7 @@ resource natGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = i resource natGateway 'Microsoft.Network/natGateways@2023-11-01' = if (hub.options.natGateway) { name: natGatewayName location: hub.location - tags: getHubTags(hub, 'Microsoft.Storage/natGateways') + tags: getHubTags(hub, 'Microsoft.Network/natGateways') sku: { name: 'Standard' } From 4ffb342a19ee6c4f7f09d31bae4b895b9274d8a4 Mon Sep 17 00:00:00 2001 From: msbrett Date: Wed, 17 Jun 2026 11:25:57 -0700 Subject: [PATCH 05/10] fix(powershell): gate enableNatGateway to v15.0+ to prevent ARM errors with older templates The enableNatGateway parameter was incorrectly gated to version 0.7+, but it only exists in the v15.0 template. This would cause ARM deployment failures for versions 0.7-14 because ARM rejects undefined template parameters. Changes: - Moved enableNatGateway from 0.7+ gate to new 15.0+ gate (matching pattern at lines 315, 320) - Added early-fail validation to prevent -NetworkMode 'private' with versions < 15.0 - Provides clear error: 'NAT Gateway / private network mode requires hub template version 15.0 or later' Tests: All 12 existing Pester unit tests pass (4 skipped) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/powershell/Public/Deploy-FinOpsHub.ps1 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/powershell/Public/Deploy-FinOpsHub.ps1 b/src/powershell/Public/Deploy-FinOpsHub.ps1 index e5af58495..7e99acaff 100644 --- a/src/powershell/Public/Deploy-FinOpsHub.ps1 +++ b/src/powershell/Public/Deploy-FinOpsHub.ps1 @@ -257,6 +257,13 @@ function Deploy-FinOpsHub # Init deployment (register providers) Initialize-FinOpsHubDeployment -WhatIf:$WhatIfPreference + # Validate version compatibility for private network mode + $effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' } + if ($effectiveNetworkMode -eq 'private' -and $Version -ne 'latest' -and [version]$Version -lt '15.0') + { + throw "NAT Gateway / private network mode requires hub template version 15.0 or later. Current version: $Version" + } + # Download template if (Test-ShouldProcess $PSCmdlet $Version 'DownloadTemplate') { @@ -293,7 +300,6 @@ function Deploy-FinOpsHub $effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' } if (-not $NetworkMode -and $DisablePublicAccess) { Write-Warning "-DisablePublicAccess is deprecated; use -NetworkMode 'vnet' or -NetworkMode 'private'." } $parameterSplat.TemplateParameterObject.Add('enablePublicAccess', ($effectiveNetworkMode -eq 'public')) - $parameterSplat.TemplateParameterObject.Add('enableNatGateway', ($effectiveNetworkMode -eq 'private')) $parameterSplat.TemplateParameterObject.Add('virtualNetworkAddressPrefix', $VirtualNetworkAddressPrefix) $parameterSplat.TemplateParameterObject.Add('exportRetentionInDays', $ExportRetentionInDays) $parameterSplat.TemplateParameterObject.Add('ingestionRetentionInMonths', $IngestionRetentionInMonths) @@ -316,6 +322,13 @@ function Deploy-FinOpsHub $parameterSplat.TemplateParameterObject.Add('enablePurgeProtection', $EnablePurgeProtection.IsPresent) } + if ($Version -eq 'latest' -or [version]$Version -ge '15.0') + { + # Resolve network mode for enableNatGateway (private mode requires NAT Gateway) + $effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' } + $parameterSplat.TemplateParameterObject.Add('enableNatGateway', ($effectiveNetworkMode -eq 'private')) + } + if ($Tags -and $Tags.Keys.Count -gt 0) { $parameterSplat.TemplateParameterObject.Add('tags', $Tags) From 58e45a4f4ca345e62432f338eb17b5ebaff9ba47 Mon Sep 17 00:00:00 2001 From: MSBrett <24294904+MSBrett@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:09:55 -0700 Subject: [PATCH 06/10] fix(hubs): correct vNet tag namespace, conditional NAT gateway, and portal tags Addresses review feedback: use Microsoft.Network/virtualNetworks tag namespace, attach natGateway conditionally instead of emitting null, add Public IP and NAT Gateway to portal TagsByResource, and document opt-in NAT cost in changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs-mslearn/toolkit/changelog.md | 7 ++++++- .../finops-hub/createUiDefinition.json | 2 ++ .../Core/infrastructure.bicep | 18 +++++++++++------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 04f1298d2..88e3624fd 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 05/19/2026 +ms.date: 06/17/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -25,6 +25,11 @@ This article summarizes the features and enhancements in each release of the Fin The following section lists features and enhancements that are currently in development. +### [FinOps hubs](hubs/finops-hubs-overview.md) + +- **Added** + - Added opt-in NAT Gateway support for private and vNet network modes; NAT Gateway incurs additional cost when enabled ([#2163](https://github.com/microsoft/finops-toolkit/pull/2163)). + -->
diff --git a/src/templates/finops-hub/createUiDefinition.json b/src/templates/finops-hub/createUiDefinition.json index a07611180..d7ea79097 100644 --- a/src/templates/finops-hub/createUiDefinition.json +++ b/src/templates/finops-hub/createUiDefinition.json @@ -981,6 +981,8 @@ "Microsoft.Network/privateDnsZones", "Microsoft.Network/privateDnsZones/virtualNetworkLinks", "Microsoft.Network/privateEndpoints", + "Microsoft.Network/publicIPAddresses", + "Microsoft.Network/natGateways", "Microsoft.Resources/deploymentScripts", "Microsoft.Storage/storageAccounts" ] diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep index f269cf9eb..85b5e9591 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep @@ -46,9 +46,11 @@ var subnets = !hub.options.privateRouting ? [] : [ properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 28, 1) defaultOutboundAccess: !hub.options.natGateway - natGateway: hub.options.natGateway ? { - id: resourceId('Microsoft.Network/natGateways', natGatewayName) - } : null + ...(hub.options.natGateway ? { + natGateway: { + id: resourceId('Microsoft.Network/natGateways', natGatewayName) + } + } : {}) networkSecurityGroup: { id: nsg.id } @@ -72,9 +74,11 @@ var subnets = !hub.options.privateRouting ? [] : [ properties: { addressPrefix: cidrSubnet(hub.options.networkAddressPrefix, 27, 1) defaultOutboundAccess: !hub.options.natGateway - natGateway: hub.options.natGateway ? { - id: resourceId('Microsoft.Network/natGateways', natGatewayName) - } : null + ...(hub.options.natGateway ? { + natGateway: { + id: resourceId('Microsoft.Network/natGateways', natGatewayName) + } + } : {}) networkSecurityGroup: { id: nsg.id } @@ -182,7 +186,7 @@ resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = if (hub.opti resource vNet 'Microsoft.Network/virtualNetworks@2023-11-01' = if (hub.options.privateRouting) { name: hub.routing.networkName location: hub.location - tags: getHubTags(hub, 'Microsoft.Storage/virtualNetworks') + tags: getHubTags(hub, 'Microsoft.Network/virtualNetworks') dependsOn: hub.options.natGateway ? [ natGateway ] : [] From 565070e97c8481c913e9222251eeae50e7cbb57b Mon Sep 17 00:00:00 2001 From: MSBrett <24294904+MSBrett@users.noreply.github.com> Date: Sun, 21 Jun 2026 08:59:48 -0700 Subject: [PATCH 07/10] fix(hubs): scope enableNatGateway to private mode and add NetworkMode tests Only pass the enableNatGateway template parameter when private network mode is requested, so public/vnet deployments stay compatible with template versions that predate the parameter. Consolidate network-mode resolution and the -DisablePublicAccess deprecation warning to a single location. Add Pester coverage for -NetworkMode mapping (public/vnet/private), the deprecated -DisablePublicAccess to vnet mapping and its precedence, and the private-mode version guard. Bump changelog ms.date to today per docs-mslearn convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs-mslearn/toolkit/changelog.md | 2 +- src/powershell/Public/Deploy-FinOpsHub.ps1 | 16 ++--- .../Tests/Unit/Deploy-FinOpsHub.Tests.ps1 | 66 +++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 88e3624fd..1abb1c9ac 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 06/17/2026 +ms.date: 06/21/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/src/powershell/Public/Deploy-FinOpsHub.ps1 b/src/powershell/Public/Deploy-FinOpsHub.ps1 index 7e99acaff..5f62f99ad 100644 --- a/src/powershell/Public/Deploy-FinOpsHub.ps1 +++ b/src/powershell/Public/Deploy-FinOpsHub.ps1 @@ -257,8 +257,11 @@ function Deploy-FinOpsHub # Init deployment (register providers) Initialize-FinOpsHubDeployment -WhatIf:$WhatIfPreference - # Validate version compatibility for private network mode + # Resolve network mode. -NetworkMode wins; -DisablePublicAccess (deprecated) maps to 'vnet'. $effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' } + if (-not $NetworkMode -and $DisablePublicAccess) { Write-Warning "-DisablePublicAccess is deprecated; use -NetworkMode 'vnet' or -NetworkMode 'private'." } + + # Private network mode (NAT Gateway) requires hub template version 15.0 or later if ($effectiveNetworkMode -eq 'private' -and $Version -ne 'latest' -and [version]$Version -lt '15.0') { throw "NAT Gateway / private network mode requires hub template version 15.0 or later. Current version: $Version" @@ -296,9 +299,6 @@ function Deploy-FinOpsHub $parameterSplat.TemplateParameterObject.Add('dataExplorerCapacity', $DataExplorerCapacity) $parameterSplat.TemplateParameterObject.Add('dataExplorerRawRetentionInDays', $DataExplorerRawRetentionInDays) $parameterSplat.TemplateParameterObject.Add('dataExplorerFinalRetentionInMonths', $DataExplorerFinalRetentionInMonths) - # Resolve network mode. -NetworkMode wins; -DisablePublicAccess (deprecated) maps to 'vnet'. - $effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' } - if (-not $NetworkMode -and $DisablePublicAccess) { Write-Warning "-DisablePublicAccess is deprecated; use -NetworkMode 'vnet' or -NetworkMode 'private'." } $parameterSplat.TemplateParameterObject.Add('enablePublicAccess', ($effectiveNetworkMode -eq 'public')) $parameterSplat.TemplateParameterObject.Add('virtualNetworkAddressPrefix', $VirtualNetworkAddressPrefix) $parameterSplat.TemplateParameterObject.Add('exportRetentionInDays', $ExportRetentionInDays) @@ -322,11 +322,11 @@ function Deploy-FinOpsHub $parameterSplat.TemplateParameterObject.Add('enablePurgeProtection', $EnablePurgeProtection.IsPresent) } - if ($Version -eq 'latest' -or [version]$Version -ge '15.0') + # Only pass enableNatGateway when private mode is requested. This keeps public/vnet + # deployments compatible with template versions that predate the parameter. + if ($effectiveNetworkMode -eq 'private' -and ($Version -eq 'latest' -or [version]$Version -ge '15.0')) { - # Resolve network mode for enableNatGateway (private mode requires NAT Gateway) - $effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' } - $parameterSplat.TemplateParameterObject.Add('enableNatGateway', ($effectiveNetworkMode -eq 'private')) + $parameterSplat.TemplateParameterObject.Add('enableNatGateway', $true) } if ($Tags -and $Tags.Keys.Count -gt 0) diff --git a/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 b/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 index 45cde0758..596b4058e 100644 --- a/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 +++ b/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 @@ -202,5 +202,71 @@ InModuleScope 'FinOpsToolkit' { } } } + + Context 'Network mode' { + BeforeAll { + Mock -CommandName 'Get-AzResourceGroup' -MockWith { return @{ ResourceGroupName = $rgName } } + Mock -CommandName 'New-AzResourceGroup' + Mock -CommandName 'Save-FinOpsHubTemplate' + Mock -CommandName 'Initialize-FinOpsHubDeployment' + $templateFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'FinOps/finops-hub-v1.0.0/main.bicep' + Mock -CommandName 'Get-ChildItem' -MockWith { return @{ FullName = $templateFile } } + Mock -CommandName 'New-AzResourceGroupDeployment' + } + + It 'Should default to public access without a NAT Gateway' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Not -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.enablePublicAccess -eq $true -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') + } -Times 1 + } + + It "Should map -NetworkMode 'public' to enablePublicAccess and no NAT Gateway" { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'public' -Version 'latest' } | Should -Not -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.enablePublicAccess -eq $true -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') + } -Times 1 + } + + It "Should map -NetworkMode 'vnet' to private access without a NAT Gateway" { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'vnet' -Version 'latest' } | Should -Not -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.enablePublicAccess -eq $false -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') + } -Times 1 + } + + It "Should map -NetworkMode 'private' to private access with a NAT Gateway" { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version 'latest' } | Should -Not -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.enablePublicAccess -eq $false -and $TemplateParameterObject.enableNatGateway -eq $true + } -Times 1 + } + + It 'Should map deprecated -DisablePublicAccess to vnet (no NAT Gateway)' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -DisablePublicAccess -Version 'latest' -WarningAction 'SilentlyContinue' } | Should -Not -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.enablePublicAccess -eq $false -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') + } -Times 1 + } + + It 'Should let -NetworkMode win over deprecated -DisablePublicAccess' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -DisablePublicAccess -Version 'latest' } | Should -Not -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.enablePublicAccess -eq $false -and $TemplateParameterObject.enableNatGateway -eq $true + } -Times 1 + } + + It 'Should throw when private mode targets a version older than 15.0' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version '14.0' } | Should -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -Times 0 + } + + It 'Should pass enableNatGateway when private mode targets version 15.0 or later' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version '15.0' } | Should -Not -Throw + Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.enableNatGateway -eq $true + } -Times 1 + } + } } } \ No newline at end of file From 27706d702cadf68436d232c46635959a385120b0 Mon Sep 17 00:00:00 2001 From: MSBrett <24294904+MSBrett@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:27:39 -0700 Subject: [PATCH 08/10] chore: trigger PR mergeability recompute Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 2b4d188b82606eda0d82f035208dfa0ea7b314fb Mon Sep 17 00:00:00 2001 From: msbrett Date: Tue, 7 Jul 2026 10:17:49 -0700 Subject: [PATCH 09/10] Fix Pester test compatibility Load Pester explicitly for unit tests, add an Assert-MockCalled fallback adapter, and make DocsLinks discovery tolerate empty match sets across Pester versions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/powershell/Tests/Initialize-Tests.ps1 | 26 ++++++++++ src/powershell/Tests/Unit/DocsLinks.Tests.ps1 | 50 ++++++++++++++----- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/powershell/Tests/Initialize-Tests.ps1 b/src/powershell/Tests/Initialize-Tests.ps1 index 51c3f5864..2053a0012 100644 --- a/src/powershell/Tests/Initialize-Tests.ps1 +++ b/src/powershell/Tests/Initialize-Tests.ps1 @@ -3,6 +3,32 @@ Remove-Module FinOpsToolkit -ErrorAction SilentlyContinue Import-Module -FullyQualifiedName "$PSScriptRoot/../FinOpsToolkit.psm1" +Import-Module Pester -Global -ErrorAction Stop + +if (-not (Get-Command Assert-MockCalled -CommandType Function -ErrorAction SilentlyContinue)) { + function Assert-MockCalled { + [CmdletBinding(DefaultParameterSetName = 'ParameterFilter')] + param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$CommandName, + + [Parameter(Position = 1)] + [int]$Times = 1, + + [ScriptBlock]$ParameterFilter = { $true }, + + [Parameter(ParameterSetName = 'ExclusiveFilter', Mandatory = $true)] + [scriptblock]$ExclusiveFilter, + + [string]$ModuleName, + + [string]$Scope = 0, + [switch]$Exactly + ) + + Should -Invoke @PSBoundParameters + } +} BeforeAll { # Bring the Monitor functions in to simplify debugging diff --git a/src/powershell/Tests/Unit/DocsLinks.Tests.ps1 b/src/powershell/Tests/Unit/DocsLinks.Tests.ps1 index 5d92fa414..e25c3ecbd 100644 --- a/src/powershell/Tests/Unit/DocsLinks.Tests.ps1 +++ b/src/powershell/Tests/Unit/DocsLinks.Tests.ps1 @@ -286,30 +286,56 @@ Describe 'Documentation links' { } Context 'docs-mslearn: No learn.microsoft.com URLs' { - - It 'Should not contain https://learn.microsoft.com links: :' -ForEach ($mslearnUrls | Where-Object { $_.Url -match 'learn\.microsoft\.com' }) { - $Url | Should -Not -Match 'learn\.microsoft\.com' -Because "links in docs-mslearn should use root-relative paths (e.g., /azure/...) instead of full URLs since docs are deployed to learn.microsoft.com (${SourceRel}:${LineNumber})" + $learnMicrosoftLinks = @($mslearnUrls | Where-Object { $_.Url -match 'learn\.microsoft\.com' }) + if ($learnMicrosoftLinks.Count -gt 0) { + It 'Should not contain https://learn.microsoft.com links: :' -ForEach $learnMicrosoftLinks { + $Url | Should -Not -Match 'learn\.microsoft\.com' -Because "links in docs-mslearn should use root-relative paths (e.g., /azure/...) instead of full URLs since docs are deployed to learn.microsoft.com (${SourceRel}:${LineNumber})" + } + } + else { + It 'Should not contain https://learn.microsoft.com links' { + $learnMicrosoftLinks | Should -BeNullOrEmpty -Because 'docs-mslearn should not contain fully qualified learn.microsoft.com URLs' + } } } Context 'docs-mslearn: No language locale in MS Learn links' { - - It 'Should not contain language locale in URL: : ' -ForEach ($mslearnUrls | Where-Object { $_.Url -match 'learn\.microsoft\.com/[a-z]{2}-[a-z]{2}/' }) { - $Url | Should -Not -Match 'learn\.microsoft\.com/[a-z]{2}-[a-z]{2}/' -Because "MS Learn links should not include language locale segments like /en-us/ (${SourceRel}:${LineNumber})" + $localizedMsLearnLinks = @($mslearnUrls | Where-Object { $_.Url -match 'learn\.microsoft\.com/[a-z]{2}-[a-z]{2}/' }) + if ($localizedMsLearnLinks.Count -gt 0) { + It 'Should not contain language locale in URL: : ' -ForEach $localizedMsLearnLinks { + $Url | Should -Not -Match 'learn\.microsoft\.com/[a-z]{2}-[a-z]{2}/' -Because "MS Learn links should not include language locale segments like /en-us/ (${SourceRel}:${LineNumber})" + } + } + else { + It 'Should not contain language locale in MS Learn links' { + $localizedMsLearnLinks | Should -BeNullOrEmpty -Because 'MS Learn links should not include language locale segments like /en-us/' + } } } Context 'docs-mslearn: No known broken external URLs' { - - It 'Should not contain known broken external URL: : ' -ForEach $knownBrokenExternalUrlMatches { - $Url | Should -Not -Match $Pattern -Because "known broken external URLs should not appear in docs-mslearn content (${SourceRel}:${LineNumber})" + if ($knownBrokenExternalUrlMatches.Count -gt 0) { + It 'Should not contain known broken external URL: : ' -ForEach $knownBrokenExternalUrlMatches { + $Url | Should -Not -Match $Pattern -Because "known broken external URLs should not appear in docs-mslearn content (${SourceRel}:${LineNumber})" + } + } + else { + It 'Should not contain known broken external URLs' { + $knownBrokenExternalUrlMatches | Should -BeNullOrEmpty -Because 'docs-mslearn should not contain any known broken external URLs' + } } } Context 'docs-mslearn: No incomplete placeholder external URLs' { - - It 'Should not contain incomplete placeholder URL: : ' -ForEach $incompleteExternalUrlMatches { - $Url | Should -Not -Match $Pattern -Because "incomplete placeholder URLs should not appear in docs-mslearn content (${SourceRel}:${LineNumber})" + if ($incompleteExternalUrlMatches.Count -gt 0) { + It 'Should not contain incomplete placeholder URL: : ' -ForEach $incompleteExternalUrlMatches { + $Url | Should -Not -Match $Pattern -Because "incomplete placeholder URLs should not appear in docs-mslearn content (${SourceRel}:${LineNumber})" + } + } + else { + It 'Should not contain incomplete placeholder external URLs' { + $incompleteExternalUrlMatches | Should -BeNullOrEmpty -Because 'docs-mslearn should not contain placeholder external URLs' + } } } From 2a3b655592433540d539220c0bf13d29b2e4f873 Mon Sep 17 00:00:00 2001 From: msbrett Date: Tue, 7 Jul 2026 10:31:07 -0700 Subject: [PATCH 10/10] Migrate legacy Pester mock assertions Replace Assert-MockCalled with Should -Invoke across the PowerShell test suite so the assertions run under the current GitHub Actions Pester environment. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/powershell/Tests/Initialize-Tests.ps1 | 25 ------- .../Tests/Integration/CostExports.Tests.ps1 | 2 +- .../Tests/Unit/Deploy-FinOpsHub.Tests.ps1 | 58 ++++++++-------- .../Tests/Unit/Get-FinOpsCostExport.Tests.ps1 | 16 ++--- .../Unit/Get-FinOpsToolkitVersion.Tests.ps1 | 14 ++-- .../Initialize-FinOpsHubDeployment.Tests.ps1 | 2 +- .../Tests/Unit/New-Directory.Tests.ps1 | 8 +-- .../Tests/Unit/New-FinOpsCostExport.Tests.ps1 | 66 +++++++++---------- .../Register-FinOpsHubProviders.Tests.ps1 | 8 +-- .../Unit/Save-FinOpsHubTemplate.Tests.ps1 | 26 ++++---- .../Unit/Start-FinOpsCostExport.Tests.ps1 | 22 +++---- 11 files changed, 111 insertions(+), 136 deletions(-) diff --git a/src/powershell/Tests/Initialize-Tests.ps1 b/src/powershell/Tests/Initialize-Tests.ps1 index 2053a0012..25abeae63 100644 --- a/src/powershell/Tests/Initialize-Tests.ps1 +++ b/src/powershell/Tests/Initialize-Tests.ps1 @@ -5,31 +5,6 @@ Remove-Module FinOpsToolkit -ErrorAction SilentlyContinue Import-Module -FullyQualifiedName "$PSScriptRoot/../FinOpsToolkit.psm1" Import-Module Pester -Global -ErrorAction Stop -if (-not (Get-Command Assert-MockCalled -CommandType Function -ErrorAction SilentlyContinue)) { - function Assert-MockCalled { - [CmdletBinding(DefaultParameterSetName = 'ParameterFilter')] - param( - [Parameter(Mandatory = $true, Position = 0)] - [string]$CommandName, - - [Parameter(Position = 1)] - [int]$Times = 1, - - [ScriptBlock]$ParameterFilter = { $true }, - - [Parameter(ParameterSetName = 'ExclusiveFilter', Mandatory = $true)] - [scriptblock]$ExclusiveFilter, - - [string]$ModuleName, - - [string]$Scope = 0, - [switch]$Exactly - ) - - Should -Invoke @PSBoundParameters - } -} - BeforeAll { # Bring the Monitor functions in to simplify debugging . "$PSScriptRoot/../../scripts/Monitor.ps1" diff --git a/src/powershell/Tests/Integration/CostExports.Tests.ps1 b/src/powershell/Tests/Integration/CostExports.Tests.ps1 index 6af8a6bc6..c0f9dc633 100644 --- a/src/powershell/Tests/Integration/CostExports.Tests.ps1 +++ b/src/powershell/Tests/Integration/CostExports.Tests.ps1 @@ -337,7 +337,7 @@ Describe 'CostExports' { -Backfill 12 # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Write-Progress' -Times 4 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Write-Progress' -Times 4 $success | Should -Be $true ((Get-Date) - $testStartTime).TotalSeconds | Should -BeGreaterThan 60 } diff --git a/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 b/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 index 596b4058e..1bb4512a0 100644 --- a/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 +++ b/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 @@ -33,9 +33,9 @@ InModuleScope 'FinOpsToolkit' { Deploy-FinOpsHub -WhatIf -Name $hubName -ResourceGroupName $rgName -Location $location # Assert - Assert-MockCalled -CommandName 'Initialize-FinOpsHubDeployment' -Times 1 -ParameterFilter { $WhatIf -eq $true } + Should -Invoke -CommandName 'Initialize-FinOpsHubDeployment' -Times 1 -ParameterFilter { $WhatIf -eq $true } @('CreateResourceGroup', 'CreateTempDirectory', 'DownloadTemplate', 'DeployFinOpsHub') | ForEach-Object { - Assert-MockCalled -CommandName 'Test-ShouldProcess' -Times 1 -ParameterFilter { $Action -eq $_ } + Should -Invoke -CommandName 'Test-ShouldProcess' -Times 1 -ParameterFilter { $Action -eq $_ } } } } @@ -55,8 +55,8 @@ InModuleScope 'FinOpsToolkit' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location # Assert - Assert-MockCalled -CommandName 'Get-AzResourceGroup' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroup' -Times 1 + Should -Invoke -CommandName 'Get-AzResourceGroup' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroup' -Times 1 } It 'Should use RG if it exists' { @@ -69,8 +69,8 @@ InModuleScope 'FinOpsToolkit' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location # Assert - Assert-MockCalled -CommandName 'Get-AzResourceGroup' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroup' -Times 0 + Should -Invoke -CommandName 'Get-AzResourceGroup' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroup' -Times 0 } } @@ -85,7 +85,7 @@ InModuleScope 'FinOpsToolkit' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location # Assert - Assert-MockCalled -CommandName 'Initialize-FinOpsHubDeployment' -Times 1 + Should -Invoke -CommandName 'Initialize-FinOpsHubDeployment' -Times 1 } } @@ -111,7 +111,7 @@ InModuleScope 'FinOpsToolkit' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location # Assert - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -Times 1 } It 'Should add tags to the deployment' -Skip { } @@ -130,7 +130,7 @@ InModuleScope 'FinOpsToolkit' { It 'Should throw if template file is not found' { Mock -CommandName 'Get-ChildItem' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Throw - Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1 + Should -Invoke -CommandName 'Get-ChildItem' -Times 1 } Context 'More' { @@ -142,14 +142,14 @@ InModuleScope 'FinOpsToolkit' { It 'Should deploy the template without throwing' { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { @{ TemplateFile = $templateFile } } -Times 1 + Should -Invoke -CommandName 'Get-ChildItem' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { @{ TemplateFile = $templateFile } } -Times 1 } It 'Should deploy the template with tags' { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Tags @{ Test = 'Tag' } -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'Get-ChildItem' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { @{ TemplateParameterObject = @{ tags = @{ @@ -163,8 +163,8 @@ InModuleScope 'FinOpsToolkit' { It 'Should deploy the template with StorageSku' { $storageSku = 'Premium_ZRS' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -StorageSku $storageSku -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'Get-ChildItem' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { @{ TemplateParameterObject = @{ storageSku = $storageSku @@ -176,8 +176,8 @@ InModuleScope 'FinOpsToolkit' { It 'Should deploy the template with RemoteHubStorageUri' { $remoteHubStorageUri = 'https://primaryhub.dfs.core.windows.net/' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -RemoteHubStorageUri $remoteHubStorageUri -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'Get-ChildItem' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.remoteHubStorageUri -eq $remoteHubStorageUri } -Times 1 } @@ -185,8 +185,8 @@ InModuleScope 'FinOpsToolkit' { It 'Should deploy the template with RemoteHubStorageKey' { $remoteHubStorageKey = 'abc123...xyz789==' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -RemoteHubStorageKey $remoteHubStorageKey -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'Get-ChildItem' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.remoteHubStorageKey -eq $remoteHubStorageKey } -Times 1 } @@ -195,8 +195,8 @@ InModuleScope 'FinOpsToolkit' { $remoteHubStorageUri = 'https://primaryhub.dfs.core.windows.net/' $remoteHubStorageKey = 'abc123...xyz789==' { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -RemoteHubStorageUri $remoteHubStorageUri -RemoteHubStorageKey $remoteHubStorageKey -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'Get-ChildItem' -Times 1 - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'Get-ChildItem' -Times 1 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.remoteHubStorageUri -eq $remoteHubStorageUri -and $TemplateParameterObject.remoteHubStorageKey -eq $remoteHubStorageKey } -Times 1 } @@ -216,54 +216,54 @@ InModuleScope 'FinOpsToolkit' { It 'Should default to public access without a NAT Gateway' { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.enablePublicAccess -eq $true -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') } -Times 1 } It "Should map -NetworkMode 'public' to enablePublicAccess and no NAT Gateway" { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'public' -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.enablePublicAccess -eq $true -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') } -Times 1 } It "Should map -NetworkMode 'vnet' to private access without a NAT Gateway" { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'vnet' -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.enablePublicAccess -eq $false -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') } -Times 1 } It "Should map -NetworkMode 'private' to private access with a NAT Gateway" { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.enablePublicAccess -eq $false -and $TemplateParameterObject.enableNatGateway -eq $true } -Times 1 } It 'Should map deprecated -DisablePublicAccess to vnet (no NAT Gateway)' { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -DisablePublicAccess -Version 'latest' -WarningAction 'SilentlyContinue' } | Should -Not -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.enablePublicAccess -eq $false -and -not $TemplateParameterObject.ContainsKey('enableNatGateway') } -Times 1 } It 'Should let -NetworkMode win over deprecated -DisablePublicAccess' { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -DisablePublicAccess -Version 'latest' } | Should -Not -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.enablePublicAccess -eq $false -and $TemplateParameterObject.enableNatGateway -eq $true } -Times 1 } It 'Should throw when private mode targets a version older than 15.0' { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version '14.0' } | Should -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -Times 0 + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -Times 0 } It 'Should pass enableNatGateway when private mode targets version 15.0 or later' { { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -NetworkMode 'private' -Version '15.0' } | Should -Not -Throw - Assert-MockCalled -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { $TemplateParameterObject.enableNatGateway -eq $true } -Times 1 } diff --git a/src/powershell/Tests/Unit/Get-FinOpsCostExport.Tests.ps1 b/src/powershell/Tests/Unit/Get-FinOpsCostExport.Tests.ps1 index 7a528284b..3abc4a85e 100644 --- a/src/powershell/Tests/Unit/Get-FinOpsCostExport.Tests.ps1 +++ b/src/powershell/Tests/Unit/Get-FinOpsCostExport.Tests.ps1 @@ -135,7 +135,7 @@ InModuleScope 'FinOpsToolkit' { $result | Should -HaveCount 1 # The individual GET endpoint was queried for the export's full run history - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Uri -match 'exports/test-export\?' -and $Uri -match '\$expand=runHistory' } @@ -175,7 +175,7 @@ InModuleScope 'FinOpsToolkit' { Get-FinOpsCostExport -Scope $scope -RunHistory # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Uri -match '\$expand=runHistory' } } @@ -208,8 +208,8 @@ InModuleScope 'FinOpsToolkit' { # Assert # Only the list call should be made (no $expand, no per-export GET) - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -Exactly - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -Exactly + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Uri -notmatch '\$expand=runHistory' } } @@ -257,7 +257,7 @@ InModuleScope 'FinOpsToolkit' { # Assert # The per-export individual GET must be attempted before falling back - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Uri -match 'exports/test-export\?' -and $Uri -match '\$expand=runHistory' } $result | Should -Not -BeNullOrEmpty @@ -310,7 +310,7 @@ InModuleScope 'FinOpsToolkit' { # Assert # The cmdlet must still return data (the thrown error is caught and the # list run history is used). - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Uri -match 'exports/test-export\?' -and $Uri -match '\$expand=runHistory' } $result | Should -Not -BeNullOrEmpty @@ -394,10 +394,10 @@ InModuleScope 'FinOpsToolkit' { # Assert # The throttled GET was retried (2 calls), waited once, and the full # history (2 runs) -- not the truncated list (1 run) -- was returned. - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 2 -Exactly -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 2 -Exactly -ParameterFilter { $Uri -match 'exports/test-export\?' } - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Start-Sleep' -Times 1 -Exactly + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Start-Sleep' -Times 1 -Exactly $result.RunHistory | Should -HaveCount 2 $result.RunHistory[1].RunId | Should -Be 'run2' } diff --git a/src/powershell/Tests/Unit/Get-FinOpsToolkitVersion.Tests.ps1 b/src/powershell/Tests/Unit/Get-FinOpsToolkitVersion.Tests.ps1 index 8482cd598..9c67430ed 100644 --- a/src/powershell/Tests/Unit/Get-FinOpsToolkitVersion.Tests.ps1 +++ b/src/powershell/Tests/Unit/Get-FinOpsToolkitVersion.Tests.ps1 @@ -103,13 +103,13 @@ InModuleScope 'FinOpsToolkit' { It 'Should return 1 result when [Latest] is used' { $result = Get-FinOpsToolkitVersion -Latest $result.Count | Should -Be 1 - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 1 } It 'Should return all versions if [Latest] not used' { $result = Get-FinOpsToolkitVersion $result.Count | Should -Be 2 - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 1 } } @@ -142,8 +142,8 @@ InModuleScope 'FinOpsToolkit' { It 'Should throw if the Uri is unreachable' { { Get-FinOpsToolkitVersion } | Should -Throw - Assert-MockCalled -CommandName 'Invoke-WebRequest' - Assert-MockCalled -CommandName 'New-Object' -Times 0 + Should -Invoke -CommandName 'Invoke-WebRequest' + Should -Invoke -CommandName 'New-Object' -Times 0 } } @@ -158,13 +158,13 @@ InModuleScope 'FinOpsToolkit' { It 'Should include prereleases when [Preview] is used' { $result = Get-FinOpsToolkitVersion -Preview $result.Count | Should -Be 2 - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 1 } It 'Should not include prereleases when [Preview] is not used' { $result = Get-FinOpsToolkitVersion $result.Count | Should -Be 1 - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 1 } } @@ -180,7 +180,7 @@ InModuleScope 'FinOpsToolkit' { $result = Get-FinOpsToolkitVersion -Latest -Preview $result.Count | Should -Be 1 $result.Version | Should -Be $previewVersion - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 1 } } } diff --git a/src/powershell/Tests/Unit/Initialize-FinOpsHubDeployment.Tests.ps1 b/src/powershell/Tests/Unit/Initialize-FinOpsHubDeployment.Tests.ps1 index 8bd783743..4ffe82375 100644 --- a/src/powershell/Tests/Unit/Initialize-FinOpsHubDeployment.Tests.ps1 +++ b/src/powershell/Tests/Unit/Initialize-FinOpsHubDeployment.Tests.ps1 @@ -19,7 +19,7 @@ InModuleScope FinOpsToolkit { Initialize-FinOpsHubDeployment # Assert - Assert-MockCalled -CommandName 'Register-FinOpsHubProviders' -Times 1 + Should -Invoke -CommandName 'Register-FinOpsHubProviders' -Times 1 } } } diff --git a/src/powershell/Tests/Unit/New-Directory.Tests.ps1 b/src/powershell/Tests/Unit/New-Directory.Tests.ps1 index 13978560a..0f7f5c3ad 100644 --- a/src/powershell/Tests/Unit/New-Directory.Tests.ps1 +++ b/src/powershell/Tests/Unit/New-Directory.Tests.ps1 @@ -105,8 +105,8 @@ InModuleScope 'FinOpsToolkit' { New-Directory -Path $path # Assert - Assert-MockCalled -CommandName 'Test-Path' - Assert-MockCalled -CommandName 'New-Item' -Times 0 + Should -Invoke -CommandName 'Test-Path' + Should -Invoke -CommandName 'New-Item' -Times 0 } It 'Should create a directory if it does not exist' { @@ -117,8 +117,8 @@ InModuleScope 'FinOpsToolkit' { New-Directory -Path $path # Assert - Assert-MockCalled -CommandName 'Test-Path' - Assert-MockCalled -CommandName 'New-Item' -Times 1 + Should -Invoke -CommandName 'Test-Path' + Should -Invoke -CommandName 'New-Item' -Times 1 } } } diff --git a/src/powershell/Tests/Unit/New-FinOpsCostExport.Tests.ps1 b/src/powershell/Tests/Unit/New-FinOpsCostExport.Tests.ps1 index 5a3c27812..e65fd986f 100644 --- a/src/powershell/Tests/Unit/New-FinOpsCostExport.Tests.ps1 +++ b/src/powershell/Tests/Unit/New-FinOpsCostExport.Tests.ps1 @@ -42,8 +42,8 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -CommandName 'Get-AzResourceProvider' -Times 1 - Assert-MockCalled -CommandName 'Register-AzResourceProvider' -Times 1 + Should -Invoke -CommandName 'Get-AzResourceProvider' -Times 1 + Should -Invoke -CommandName 'Register-AzResourceProvider' -Times 1 } It 'Should create export' { @@ -52,11 +52,11 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -CommandName 'Get-AzResourceProvider' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 + Should -Invoke -CommandName 'Get-AzResourceProvider' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 # TODO: Validate request body via parameter filter in Invoke-Rest call - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 0 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 0 } It 'Should create and run scheduled export' { @@ -65,11 +65,11 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -Execute # Assert - Assert-MockCalled -CommandName 'Get-AzResourceProvider' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 + Should -Invoke -CommandName 'Get-AzResourceProvider' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 # TODO: Validate request body via parameter filter in Invoke-Rest call - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 1 } It 'Should create and backfill scheduled export' { @@ -80,11 +80,11 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -Backfill $backfillMonths # Assert - Assert-MockCalled -CommandName 'Get-AzResourceProvider' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 + Should -Invoke -CommandName 'Get-AzResourceProvider' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 # TODO: Validate request body via parameter filter in Invoke-Rest call - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 1 -ParameterFilter { $Backfill -eq $backfillMonths } + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 1 -ParameterFilter { $Backfill -eq $backfillMonths } } It 'Should create and run one-time export' { @@ -93,11 +93,11 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -OneTime # Assert - Assert-MockCalled -CommandName 'Get-AzResourceProvider' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 + Should -Invoke -CommandName 'Get-AzResourceProvider' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 2 # TODO: Validate request body via parameter filter in Invoke-Rest call - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Start-FinOpsCostExport' -Times 1 } } @@ -108,7 +108,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Uri -match 'api-version=2025-03-01' } } @@ -119,7 +119,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.definition.type -eq 'FocusCost' } } @@ -130,7 +130,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $null -eq $Body.identity } } @@ -141,7 +141,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.schedule.status -eq 'Active' } } @@ -161,7 +161,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -Dataset $_.dataset # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.definition.type -eq $_.dataset ` -and $Body.properties.schedule.recurrence -eq $_.schedule } @@ -173,7 +173,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -Dataset $_.dataset # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.definition.type -eq $_.dataset ` -and $Body.properties.definition.dataSet.configuration.dataVersion -eq $_.version } @@ -188,7 +188,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -SystemAssignedIdentity # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.identity.type -eq 'SystemAssigned' ` -and $Body.location -eq 'global' } @@ -202,7 +202,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -SystemAssignedIdentity -Location $location # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.identity.type -eq 'SystemAssigned' ` -and $Body.location -eq $location } @@ -216,7 +216,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.format -eq 'Csv' } } @@ -227,7 +227,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -Format 'Parquet' # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.format -eq 'Parquet' } } @@ -238,7 +238,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.compressionMode -eq 'None' } } @@ -249,7 +249,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @newExportParams -CompressionMode 'Snappy' # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.compressionMode -eq 'Snappy' } } @@ -269,7 +269,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @paramsWithColons # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.deliveryInfo.destination.rootFolderPath -eq (($scopeWithColons -replace ':','-').Trim('/')) } } @@ -289,7 +289,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @paramsWithExplicitPath # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.deliveryInfo.destination.rootFolderPath -eq $explicitPathWithColons } } @@ -307,7 +307,7 @@ InModuleScope 'FinOpsToolkit' { New-FinOpsCostExport @paramsWithoutColons # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $Body.properties.deliveryInfo.destination.rootFolderPath -eq $scopeWithoutColons.Trim('/') } } diff --git a/src/powershell/Tests/Unit/Register-FinOpsHubProviders.Tests.ps1 b/src/powershell/Tests/Unit/Register-FinOpsHubProviders.Tests.ps1 index 7d5ef11de..80de71465 100644 --- a/src/powershell/Tests/Unit/Register-FinOpsHubProviders.Tests.ps1 +++ b/src/powershell/Tests/Unit/Register-FinOpsHubProviders.Tests.ps1 @@ -21,8 +21,8 @@ InModuleScope 'FinOpsToolkit' { # Assert $requiredRPs | ForEach-Object { - Assert-MockCalled -CommandName 'Get-AzResourceProvider' -Times 1 -ParameterFilter { $ProviderNamespace -eq $_ } - Assert-MockCalled -CommandName 'Register-AzResourceProvider' -Times 1 -ParameterFilter { $ProviderNamespace -eq $_ -and $WhatIf -eq $true } + Should -Invoke -CommandName 'Get-AzResourceProvider' -Times 1 -ParameterFilter { $ProviderNamespace -eq $_ } + Should -Invoke -CommandName 'Register-AzResourceProvider' -Times 1 -ParameterFilter { $ProviderNamespace -eq $_ -and $WhatIf -eq $true } } } } @@ -40,8 +40,8 @@ InModuleScope 'FinOpsToolkit' { Register-FinOpsHubProviders # Assert - Assert-MockCalled -CommandName 'Get-AzResourceProvider' -Times $requiredRPs.Count - Assert-MockCalled -CommandName 'Register-AzResourceProvider' -Times ($state -eq 'Registered' ? 0 : $requiredRPs.Count) + Should -Invoke -CommandName 'Get-AzResourceProvider' -Times $requiredRPs.Count + Should -Invoke -CommandName 'Register-AzResourceProvider' -Times ($state -eq 'Registered' ? 0 : $requiredRPs.Count) } } } diff --git a/src/powershell/Tests/Unit/Save-FinOpsHubTemplate.Tests.ps1 b/src/powershell/Tests/Unit/Save-FinOpsHubTemplate.Tests.ps1 index 7bfc93ec7..98a31dec0 100644 --- a/src/powershell/Tests/Unit/Save-FinOpsHubTemplate.Tests.ps1 +++ b/src/powershell/Tests/Unit/Save-FinOpsHubTemplate.Tests.ps1 @@ -39,8 +39,8 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate # Assert - Assert-MockCalled -CommandName 'Get-FinOpsToolkitVersion' -Times 1 - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 0 + Should -Invoke -CommandName 'Get-FinOpsToolkitVersion' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 0 } } @@ -59,8 +59,8 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate # Assert - Assert-MockCalled -CommandName 'Get-FinOpsToolkitVersion' -Times 1 - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 1 + Should -Invoke -CommandName 'Get-FinOpsToolkitVersion' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 1 } } @@ -75,9 +75,9 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate -Version $downloadVersion # Assert - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 1 -ParameterFilter { $Uri -eq $downloadUrl } - Assert-MockCalled -CommandName 'Expand-Archive' -Times 1 - Assert-MockCalled -CommandName 'Remove-Item' -Times 1 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 1 -ParameterFilter { $Uri -eq $downloadUrl } + Should -Invoke -CommandName 'Expand-Archive' -Times 1 + Should -Invoke -CommandName 'Remove-Item' -Times 1 } } @@ -88,7 +88,7 @@ InModuleScope FinOpsToolkit { It 'Should throw' { { Save-FinOpsHubTemplate } | Should -Throw - Assert-MockCalled -CommandName 'Invoke-WebRequest' -Times 0 + Should -Invoke -CommandName 'Invoke-WebRequest' -Times 0 } } @@ -102,7 +102,7 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate -Version '0.2' # Assert - Assert-MockCalled -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } + Should -Invoke -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } } It 'Should not redirect 0.2 to 0.1.1 for Azure Gov' { @@ -114,7 +114,7 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate -Version '0.3' # Assert - Assert-MockCalled -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } + Should -Invoke -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } } It 'Should not redirect 0.2 to 0.1.1 for Azure China' { @@ -126,7 +126,7 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate -Version '0.3' # Assert - Assert-MockCalled -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } + Should -Invoke -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } } It 'Should support 0.3 for Azure Gov' { @@ -138,7 +138,7 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate -Version '0.3' # Assert - Assert-MockCalled -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } + Should -Invoke -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } } It 'Should support 0.3 for Azure China' { @@ -150,7 +150,7 @@ InModuleScope FinOpsToolkit { Save-FinOpsHubTemplate -Version '0.3' # Assert - Assert-MockCalled -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } + Should -Invoke -CommandName 'Test-Path' -Times 1 -ParameterFilter { $Path.EndsWith('0.3.zip') } } } } diff --git a/src/powershell/Tests/Unit/Start-FinOpsCostExport.Tests.ps1 b/src/powershell/Tests/Unit/Start-FinOpsCostExport.Tests.ps1 index fa07da945..b9d55edbd 100644 --- a/src/powershell/Tests/Unit/Start-FinOpsCostExport.Tests.ps1 +++ b/src/powershell/Tests/Unit/Start-FinOpsCostExport.Tests.ps1 @@ -31,7 +31,7 @@ Describe 'Start-FinOpsCostExport' { { Start-FinOpsCostExport @params } | Should -Throw # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Get-FinOpsCostExport' -Times 1 } It 'Should call /run with default dates' { @@ -47,7 +47,7 @@ Describe 'Start-FinOpsCostExport' { $success = Start-FinOpsCostExport @params # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` -ParameterFilter { return $body -eq $null } $success | Should -Be $true } @@ -66,7 +66,7 @@ Describe 'Start-FinOpsCostExport' { $success = Start-FinOpsCostExport @params # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` -ParameterFilter { $body.timePeriod.from -eq $params.StartDate.ToUniversalTime().ToString("yyyy-01-01'T'00:00:00'Z'") -and $body.timePeriod.to -eq $params.StartDate.ToUniversalTime().ToString("yyyy-01-31'T'00:00:00'Z'") } $success | Should -Be $true } @@ -86,7 +86,7 @@ Describe 'Start-FinOpsCostExport' { $success = Start-FinOpsCostExport @params # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` -ParameterFilter { $body.timePeriod.from -eq $params.StartDate.ToUniversalTime().Date.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") -and $body.timePeriod.to -eq $params.EndDate.ToUniversalTime().Date.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") } $success | Should -Be $true } @@ -107,15 +107,15 @@ Describe 'Start-FinOpsCostExport' { $success = Start-FinOpsCostExport @params # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times ($params.Backfill + 1) - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times ($params.Backfill + 1) + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $startDate = $startOfMonth.ToUniversalTime().Date $body.timePeriod.from -eq $startDate.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") ` -and $body.timePeriod.to -eq $today.AddDays(-1).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") } foreach ($i in 1..($params.Backfill)) { - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 -ParameterFilter { $startDate = $startOfMonth.AddMonths($i * -1).ToUniversalTime().Date $body.timePeriod.from -eq $startDate.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") ` -and $body.timePeriod.to -eq $startDate.AddMonths(1).AddMilliseconds(-1).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") @@ -137,7 +137,7 @@ Describe 'Start-FinOpsCostExport' { -Backfill 3 # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Write-Progress' -Times 4 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Write-Progress' -Times 4 $success | Should -Be $true } @@ -166,7 +166,7 @@ Describe 'Start-FinOpsCostExport' { $success = Start-FinOpsCostExport @params # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 1 ` -ParameterFilter { $body.timePeriod.from -eq $firstDayOfCurrentMonth.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") -and $body.timePeriod.to -eq $today.AddDays(-1).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") @@ -213,8 +213,8 @@ Describe 'Start-FinOpsCostExport' { $success = Start-FinOpsCostExport @params # Assert - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 2 - Assert-MockCalled -ModuleName FinOpsToolkit -CommandName 'Start-Sleep' -Times 1 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Invoke-Rest' -Times 2 + Should -Invoke -ModuleName FinOpsToolkit -CommandName 'Start-Sleep' -Times 1 $success | Should -Be $true } }