Tests/Get-MsecDefenderEmailStats.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecDefenderEmailStats. The function POSTs a KQL aggregation to # Microsoft Graph's /security/runHuntingQuery and projects the single result row # into a flat summary with percentages. Tests cover: # 1. The KQL body sent to the API filters Inbound + the right time window. # 2. Counts come through verbatim; percentages are computed correctly. # 3. Zero-result responses don't blow up; everything zeroes out. # 4. A 403 is rewritten to mention ThreatHunting.Read.All. BeforeAll { $modulePath = Join-Path $PSScriptRoot '..' 'msec.psm1' Import-Module $modulePath -Force -ErrorAction Stop $script:TestThumbBytes = [byte[]](1..20) } AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecDefenderEmailStats' { BeforeEach { InModuleScope msec -Parameters @{ Thumb = $script:TestThumbBytes } { param($Thumb) $script:MsecSession = @{ TenantId = 'tenant' ClientId = 'client' KeyVaultName = 'kv-test' KeyName = 'msec-app' ThumbprintBytes = $Thumb Tokens = @{} } } } It 'sends a KQL POST that filters Inbound + the requested -Days window, and computes percentages' { $captured = InModuleScope msec { Mock Invoke-MsecKeyVaultSign -MockWith { [byte[]](1..10) } Mock Invoke-RestMethod -ParameterFilter { $Uri -match 'oauth2/v2.0/token' } -MockWith { [pscustomobject]@{ access_token = 'mock'; expires_in = 3600 } } $script:CapturedBody = $null $script:CapturedMethod = $null Mock Invoke-RestMethod -ParameterFilter { $Uri -match '/security/runHuntingQuery' } -MockWith { $script:CapturedBody = $Body $script:CapturedMethod = $Method [pscustomobject]@{ Schema = @() Results = @( [pscustomobject]@{ Total = 1000 Delivered = 850 Junked = 100 Blocked = 40 Replaced = 10 Phishing = 25 Spam = 75 Malware = 5 } ) } } $out = Get-MsecDefenderEmailStats -Days 30 [pscustomobject]@{ Out = $out; Body = $script:CapturedBody; Method = $script:CapturedMethod } } # Method + KQL shape. Body is the JSON-encoded payload, so the patterns # use \w-friendly fragments rather than full literals with quotes (which # would be backslash-escaped in the encoded form). $captured.Method | Should -Be 'POST' $captured.Body | Should -Match 'EmailEvents' $captured.Body | Should -Match 'ago\(30d\)' # window flowed through from -Days $captured.Body | Should -Match 'EmailDirection.*Inbound' # direction filter applied $captured.Body | Should -Match 'ThreatTypes.*Phish' # threat-type filter applied # And NOT outbound - direction filter is hard-coded to Inbound. $captured.Body | Should -Not -Match 'Outbound' # Raw counts pass through. $captured.Out.Total | Should -Be 1000 $captured.Out.Delivered | Should -Be 850 $captured.Out.Junked | Should -Be 100 $captured.Out.Blocked | Should -Be 40 $captured.Out.Replaced | Should -Be 10 $captured.Out.Phishing | Should -Be 25 $captured.Out.Spam | Should -Be 75 $captured.Out.Malware | Should -Be 5 # Percentages: each / Total * 100, rounded to 2dp. $captured.Out.DeliveredPercent | Should -Be 85.0 $captured.Out.JunkedPercent | Should -Be 10.0 $captured.Out.BlockedPercent | Should -Be 4.0 $captured.Out.PhishingPercent | Should -Be 2.5 $captured.Out.SpamPercent | Should -Be 7.5 $captured.Out.MalwarePercent | Should -Be 0.5 } It 'returns all-zero counts (no division-by-zero) when the API returns no rows' { $out = InModuleScope msec { Mock Invoke-MsecKeyVaultSign -MockWith { [byte[]](1..10) } Mock Invoke-RestMethod -ParameterFilter { $Uri -match 'oauth2/v2.0/token' } -MockWith { [pscustomobject]@{ access_token = 'mock'; expires_in = 3600 } } Mock Invoke-RestMethod -ParameterFilter { $Uri -match '/security/runHuntingQuery' } -MockWith { [pscustomobject]@{ Schema = @(); Results = @() } } Get-MsecDefenderEmailStats } $out.Total | Should -Be 0 $out.Phishing | Should -Be 0 # All percentages should be 0.0, not NaN / divide-by-zero / null. $out.PhishingPercent | Should -Be 0.0 $out.DeliveredPercent | Should -Be 0.0 } It 'rewrites a 403 to mention the missing ThreatHunting.Read.All permission' { InModuleScope msec { Mock Invoke-MsecKeyVaultSign -MockWith { [byte[]](1..10) } Mock Invoke-RestMethod -ParameterFilter { $Uri -match 'oauth2/v2.0/token' } -MockWith { [pscustomobject]@{ access_token = 'mock'; expires_in = 3600 } } Mock Invoke-RestMethod -ParameterFilter { $Uri -match '/security/runHuntingQuery' } -MockWith { # Mimic Invoke-RestMethod's HttpResponseException for a 403. throw 'Response status code does not indicate success: 403 (Forbidden).' } { Get-MsecDefenderEmailStats } | Should -Throw -ExpectedMessage '*ThreatHunting.Read.All*' } } } |