tests/Get-MsecMySqlConfiguration.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecMySqlConfiguration. # # The traps, all found by calling the live endpoint before writing the command: # # EVERY BOOLEAN ON THIS ENDPOINT IS A STRING, and [bool] 'False' is $true in PowerShell. A # cast instead of a string comparison turns every read-only parameter into a settable one, # silently and with no error. 162 of 438 parameters on a current server are read-only. # # SOURCE, NOT VALUE-VERSUS-DEFAULT, SAYS WHETHER SOMEONE CHANGED A PARAMETER. Measured: # require_secure_transport with Value 'ON' and DefaultValue 'ON' and Source 'user-override'. # A deviation report built on Value -ne DefaultValue misses it. # # A SERVER WHOSE PARAMETERS CANNOT BE READ MUST NOT LOOK LIKE A SERVER WITH NONE. It is # warned about and omitted. BeforeAll { Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop $script:StubbedAz = @() $azStubs = @{ 'Get-AzContext' = { [CmdletBinding()] param() } 'Get-AzAccessToken' = { [CmdletBinding()] param($ResourceUrl) } } foreach ($c in $azStubs.Keys) { if (-not (Get-Command $c -ErrorAction SilentlyContinue)) { Set-Item "function:global:$c" -Value $azStubs[$c] -Force $script:StubbedAz += $c } } } AfterAll { foreach ($c in $script:StubbedAz) { Remove-Item "function:global:$c" -ErrorAction SilentlyContinue } Remove-Module Msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecMySqlConfiguration' { BeforeAll { # Shaped exactly like the live endpoint: every boolean is a STRING. function New-Entry { param($Name, $Value, $Default, $Source, $ReadOnly = 'False', $Pending = 'False', $Current = $null) [pscustomobject]@{ name = $Name id = "/subscriptions/s1/resourceGroups/rg/providers/Microsoft.DBforMySQL/flexibleServers/db1/configurations/$Name" properties = [pscustomobject]@{ value = $Value currentValue = if ($null -ne $Current) { $Current } else { $Value } defaultValue = $Default source = $Source isReadOnly = $ReadOnly isConfigPendingRestart = $Pending dataType = 'Enumeration' allowedValues = 'ON,OFF' description = "about $Name" documentationLink = 'https://example.invalid' } } } } BeforeEach { InModuleScope Msec { Mock Get-AzContext -MockWith { [pscustomobject]@{ Subscription = [pscustomobject]@{ Id = 's1' } } } Mock Get-AzAccessToken -MockWith { [pscustomobject]@{ Token = 'tok' } } Mock Get-MsecEnvironment -MockWith { [pscustomobject]@{ ArmResource = 'https://management.azure.com' } } Mock Search-MsecAzureResourceGraph -MockWith { @([pscustomobject]@{ Name = 'db1'; ResourceGroupName = 'rg'; SubscriptionName = 'PROD' SubscriptionId = 's1'; Version = '8.4' Id = '/subscriptions/s1/resourceGroups/rg/providers/Microsoft.DBforMySQL/flexibleServers/db1' }) } } } It "reads isReadOnly as a string - [bool] 'False' is \$true and would inverse every row" { $entries = @( (New-Entry -Name 'binlog_encryption' -Value 'OFF' -Default 'OFF' -Source 'system-default' -ReadOnly 'True'), (New-Entry -Name 'require_secure_transport' -Value 'ON' -Default 'ON' -Source 'user-override' -ReadOnly 'False') ) $rows = InModuleScope Msec -Parameters @{ Entries = $entries } { # Assigned into module scope: a -Parameters variable is NOT in scope when the mock # body below actually runs. Same trap as the Intune tests. $script:Entries = $Entries Mock Invoke-RestMethod -MockWith { [pscustomobject]@{ value = $script:Entries } } Get-MsecMySqlConfiguration } ($rows | Where-Object Parameter -eq 'binlog_encryption').IsReadOnly | Should -BeTrue ($rows | Where-Object Parameter -eq 'require_secure_transport').IsReadOnly | Should -BeFalse } It 'treats Source as the deviation signal, not Value versus DefaultValue' { # REGRESSION. require_secure_transport is the real case: ON, default ON, and explicitly # set by a person. Value -ne DefaultValue is $false here, so a report built that way # lists nothing and concludes nobody has touched the server. $entries = @( (New-Entry -Name 'require_secure_transport' -Value 'ON' -Default 'ON' -Source 'user-override'), (New-Entry -Name 'default_table_encryption' -Value 'OFF' -Default 'OFF' -Source 'system-default') ) $rows = InModuleScope Msec -Parameters @{ Entries = $entries } { $script:Entries = $Entries Mock Invoke-RestMethod -MockWith { [pscustomobject]@{ value = $script:Entries } } Get-MsecMySqlConfiguration -UserOverride } @($rows).Count | Should -Be 1 $rows[0].Parameter | Should -Be 'require_secure_transport' # The point of the test: the two values are identical and it is still an override. $rows[0].Value | Should -Be $rows[0].DefaultValue $rows[0].IsUserOverride | Should -BeTrue } It 'keeps Value and CurrentValue apart when a restart is pending' { # Value is what will apply; CurrentValue is what the engine is running. Collapsing them # claims a control is in force before the server has picked it up. $entries = @( (New-Entry -Name 'require_secure_transport' -Value 'ON' -Default 'ON' -Source 'user-override' -Pending 'True' -Current 'OFF') ) $rows = InModuleScope Msec -Parameters @{ Entries = $entries } { $script:Entries = $Entries Mock Invoke-RestMethod -MockWith { [pscustomobject]@{ value = $script:Entries } } Get-MsecMySqlConfiguration } $rows[0].Value | Should -Be 'ON' $rows[0].CurrentValue | Should -Be 'OFF' $rows[0].PendingRestart | Should -BeTrue } It 'filters parameters by wildcard, client-side' { $entries = @( (New-Entry -Name 'tls_version' -Value 'TLSv1.2' -Default 'TLSv1.2,TLSv1.3' -Source 'user-override'), (New-Entry -Name 'max_connections' -Value '10923' -Default '10923' -Source 'system-default'), (New-Entry -Name 'default_table_encryption' -Value 'OFF' -Default 'OFF' -Source 'system-default') ) $rows = InModuleScope Msec -Parameters @{ Entries = $entries } { $script:Entries = $Entries Mock Invoke-RestMethod -MockWith { [pscustomobject]@{ value = $script:Entries } } Get-MsecMySqlConfiguration -Name 'tls_*', '*encrypt*' } @($rows).Count | Should -Be 2 @($rows.Parameter) | Should -Not -Contain 'max_connections' } It 'warns and omits a server whose parameters cannot be read, rather than reporting none' { InModuleScope Msec { Mock Invoke-RestMethod -MockWith { throw 'Forbidden' } $rows = Get-MsecMySqlConfiguration -WarningVariable w -WarningAction SilentlyContinue $script:capturedRows = @($rows) $script:capturedWarn = "$w" } $rows = InModuleScope Msec { $script:capturedRows } $warn = InModuleScope Msec { $script:capturedWarn } @($rows).Count | Should -Be 0 $warn | Should -Match 'MISSING from this output' } It 'follows nextLink so a paged response is not silently truncated' { $p1 = @(New-Entry -Name 'a' -Value '1' -Default '1' -Source 'system-default') $p2 = @(New-Entry -Name 'b' -Value '2' -Default '2' -Source 'system-default') $rows = InModuleScope Msec -Parameters @{ P1 = $p1; P2 = $p2 } { $script:P1 = $P1; $script:P2 = $P2; $script:Page = 0 Mock Invoke-RestMethod -MockWith { $script:Page++ if ($script:Page -eq 1) { [pscustomobject]@{ value = $script:P1; nextLink = 'https://management.azure.com/page2' } } else { [pscustomobject]@{ value = $script:P2 } } } Get-MsecMySqlConfiguration } @($rows).Count | Should -Be 2 @($rows.Parameter) | Should -Contain 'b' } It 'throws, naming the cause, when there is no Azure context' { InModuleScope Msec { Mock Get-AzContext -MockWith { $null } { Get-MsecMySqlConfiguration } | Should -Throw '*runs as you, not as the msec app*' } } } |