Tests/VSAModule.DynamicParam.Tests.ps1
|
BeforeAll { $script:ModuleRoot = Split-Path -Path (Split-Path -Parent $PSCommandPath) Import-Module (Join-Path $script:ModuleRoot 'VSAModule.psd1') -Force } Describe "DynamicParam network calls removed (T-7.2 / F-44)" { Context "Get-Command -Syntax triggers zero transport calls" { It "<Name>" -ForEach @( @{ Name = 'Enable-VSAUser' } @{ Name = 'Remove-VSAUser' } @{ Name = 'Disable-VSATenant' } @{ Name = 'Clear-VSATenantRoleType' } @{ Name = 'Set-VSATenantRoletypeLimit' } @{ Name = 'Set-VSATenantModuleUsageType' } @{ Name = 'Set-VSATenantModuleLicense' } @{ Name = 'Update-VSAUser' } @{ Name = 'Get-VSAAudit' } ) { InModuleScope VSAModule -Parameters @{ Name = $Name } { param($Name) $script:calls = 0 Mock Invoke-VSARestMethod { $script:calls++ } Get-Command -Name $Name -Syntax | Out-Null $script:calls | Should -Be 0 } } } Context 'No script-scoped cross-block state and no DynamicParam blocks remain' { It "<Name> has neither a DynamicParam block nor script-scoped variables" -ForEach @( @{ Name = 'Enable-VSAUser' } @{ Name = 'Remove-VSAUser' } @{ Name = 'Disable-VSATenant' } @{ Name = 'Clear-VSATenantRoleType' } @{ Name = 'Set-VSATenantRoletypeLimit' } @{ Name = 'Set-VSATenantModuleUsageType' } @{ Name = 'Set-VSATenantModuleLicense' } @{ Name = 'Update-VSAUser' } ) { $path = Join-Path $script:ModuleRoot "public/$Name.ps1" $content = Get-Content -Path $path -Raw $content | Should -Not -Match '(?im)^\s*DynamicParam\s*\{' $content | Should -Not -Match '\$script:' } } Context "Single targeted Begin-block lookup resolves Name to Id" { It "Remove-VSAUser resolves AdminName to UserId with one Get-VSAUser call" { InModuleScope VSAModule { $script:calls = 0 Mock Invoke-VSARestMethod { $script:calls++ [pscustomobject]@{ UserId = '42'; AdminName = 'alice' } } Remove-VSAUser -AdminName 'alice' | Out-Null # One call to resolve AdminName->UserId, one call to perform the DELETE. $script:calls | Should -Be 2 } } It "Clear-VSATenantRoleType resolves both TenantName and RoleTypeName to Ids" { InModuleScope VSAModule { $script:capturedSuffix = $null # The role-type lookup (system/roletypes) and the write (tenantmanagement/tenant/ # roletypes) both contain "roletypes", so they must be matched distinctly: a mock that # returns nothing for the lookup lets an unresolved role type reach the write. Mock Invoke-VSARestMethod { switch -Regex ($URISuffix) { 'tenantmanagement/tenant/roletypes' { $script:capturedSuffix = $URISuffix; break } 'system/roletypes' { [pscustomobject]@{ RoleTypeId = '6'; RoleTypeName = 'VSA Admin' }; break } default { [pscustomobject]@{ Id = '99'; Ref = 'Acme' } } } } Clear-VSATenantRoleType -TenantName 'Acme' -RoleTypeName 'VSA Admin' | Out-Null # Assert BOTH ids: asserting only the tenant id let an unresolved role type reach the # wire as roleTypeId=0 (F-4). $script:capturedSuffix | Should -Match 'roletypes/99' $script:capturedSuffix | Should -Match 'roleTypeId=6' } } It "Clear-VSATenantRoleType throws rather than sending an unresolved RoleTypeName" { InModuleScope VSAModule { Mock Invoke-VSARestMethod { if ($URISuffix -match 'system/roletypes') { return } [pscustomobject]@{ Id = '99'; Ref = 'Acme' } } { Clear-VSATenantRoleType -TenantName 'Acme' -RoleTypeName 'No Such Role' } | Should -Throw '*No role type found*' } } } Context "ArgumentCompleter is attached and best-effort" { It "Remove-VSAUser's AdminName parameter has an ArgumentCompleter attribute" { $cmd = Get-Command -Name Remove-VSAUser $param = $cmd.Parameters['AdminName'] $completer = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ArgumentCompleterAttribute] } $completer | Should -Not -BeNullOrEmpty } } } Describe "Update-VSAUser ByName keeps Begin-resolved Ids in the body (F-25)" { It "resolves AdminName/RoleNames/ScopeNames/OrgName and includes the Ids in the PUT body" { InModuleScope VSAModule { Mock Update-VSAConnection {} Mock Get-VSAUser { [pscustomobject]@{ UserId = 42; AdminName = 'jdoe' } } Mock Get-VSARoles { [pscustomobject]@{ RoleId = 7; RoleName = 'Admins' } } Mock Get-VSAScope { [pscustomobject]@{ ScopeId = 9; ScopeName = 'AllScope' } } Mock Get-VSAOrganization { [pscustomobject]@{ OrgId = 3; OrgName = 'Acme' } } $script:body = $null Mock Invoke-VSARestMethod { $script:body = $Body } Update-VSAUser -AdminName 'jdoe' -AdminRoleNames 'Admins' -AdminScopeNames 'AllScope' -DefaultStaffOrgName 'Acme' | Out-Null $obj = $script:body | ConvertFrom-Json $obj.UserId | Should -Be 42 $obj.AdminRoleIds | Should -Contain 7 $obj.AdminScopeIds | Should -Contain 9 $obj.DefaultStaffOrgId | Should -Be 3 } } } Describe "Scheduling DynamicParam binds when Repeat != Never (F-33)" { # Before F-33 the helper call sites passed the type in argument position ('-Type [int]'), # which PowerShell parsed as the STRING '[int]'. Converting that to [Type] threw, so the whole # DynamicParam block failed with "Cannot retrieve the dynamic parameters for the cmdlet" and # EVERY recurring (Repeat != 'Never') schedule was unusable. These tests bind a dynamic param # (proving the block is retrievable) with the transport mocked so nothing hits the network. It "<Name> evaluates its DynamicParam block (Repeat != Never) without the [Type]-cast failure" -ForEach @( @{ Name = 'New-VSAAPScheduled'; Args = @{ AgentID='1'; AgentProcedureId='1'; EndAt='2359'; EndOn=[datetime]'2030-12-31'; Repeat='Months' } } @{ Name = 'New-VSAScheduleAuditBaseLine'; Args = @{ AgentID='1'; Repeat='Months' } } @{ Name = 'Set-VSAAuditSchedule'; Args = @{ AgentID='1'; Repeat='Months' } } @{ Name = 'Set-VSAScheduleAuditSysInfo'; Args = @{ AgentID='1'; Repeat='Months' } } @{ Name = 'Set-VSAPatchIgnore'; Args = @{ AgentID='1'; Repeat='Months' } } ) { # -Repeat 'Months' forces PowerShell to retrieve the dynamic parameters. Before the fix that # failed for ALL of these with "Cannot retrieve the dynamic parameters ... Cannot convert the # '[int]' value ... to type 'System.Type'". Each cmdlet's mandatory params are supplied so # nothing prompts, and the transport is mocked so nothing hits the network. InModuleScope VSAModule -Parameters @{ Name = $Name; CmdArgs = $Args } { param($Name, $CmdArgs) Mock Invoke-VSARestMethod { } $err = $null try { & $Name @CmdArgs -ErrorAction Stop 6>$null } catch { $err = "$($_.Exception.Message)" } $err | Should -Not -Match 'dynamic parameter' $err | Should -Not -Match "Cannot convert.*'System\.Type'" } } It "New-VSAAPScheduled binds -DayOfMonth 'LastDay' and reaches the transport (no [Type] cast error)" { InModuleScope VSAModule { $script:reached = $false Mock Invoke-VSARestMethod { $script:reached = $true } $threw = $null try { New-VSAAPScheduled -AgentID 123 -AgentProcedureId 456 -Repeat 'Months' ` -DayOfMonth 'LastDay' -EndAt '2359' -EndOn ([datetime]'2030-12-31') ` -StartOn ([datetime]'2030-01-01') | Out-Null } catch { $threw = $_.Exception.Message } # The F-33 regression signature was a binding-time failure; assert it is gone and the # body actually reached the (mocked) transport. $threw | Should -Not -Match 'dynamic parameter' $threw | Should -Not -Match 'Cannot convert.*System\.Type' $script:reached | Should -BeTrue } } } Describe "Scheduling cmdlets have no static/dynamic parameter collisions (F-36)" { # Set-VSAPatchIgnore declared EndAt/EndOn/EndAfterIntervalTimes/SpecificDayOfMonth BOTH as static # params and again in its DynamicParam block. During discovery ($Repeat unbound -> guard true) the # dynamic block runs and PowerShell throws "A parameter with the name '<x>' was defined multiple # times", leaving Get-Command's .Parameters null -- which crashed unrelated metadata tests. Guard: # every scheduling cmdlet must expose a non-null parameter set and resolve a dynamic param cleanly. It "<Name> exposes non-null .Parameters and resolves a dynamic param without a duplicate-name error" -ForEach @( @{ Name = 'New-VSAAPScheduled' } @{ Name = 'New-VSAScheduleAuditBaseLine' } @{ Name = 'Set-VSAAuditSchedule' } @{ Name = 'Set-VSAScheduleAuditSysInfo' } @{ Name = 'Set-VSAPatchIgnore' } @{ Name = 'Start-VSAPatchUpdate' } ) { $cmd = Get-Command $Name -Module VSAModule $cmd.Parameters | Should -Not -BeNullOrEmpty { $cmd.ResolveParameter('DayOfMonth') } | Should -Not -Throw } } |