tests/Get-MsecSecureScoreRecommendation.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecSecureScoreRecommendation. The cases that matter are the ones where a
# plausible-looking implementation reports something false:
# - 'on' arrives as the STRING "false", which is truthy. Enabled must be $false, not $true.
# - A profile with no matching control score is NOT a control scoring zero. CurrentScore
# must be $null for those, never 0.
# - A scored control with no published profile is still returned, with a null Title, rather
# than dropped.
# - An Ignored control is not a completed one and must still be visible.
# - Deprecated and completed controls are excluded by default - this is the
# recommended-actions list, not an inventory.

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-MsecSecureScoreRecommendation' {
    BeforeEach {
        InModuleScope Msec -Parameters @{ Thumb = $script:TestThumbBytes } {
            param($Thumb)
            $script:MsecSession = @{
                TenantId = 'tenant'; ClientId = 'client'; KeyVaultName = 'kv-test'
                KeyName = 'msec-app'; ThumbprintBytes = $Thumb; Tokens = @{}
            }

            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 'secureScoreControlProfiles' } -MockWith {
                [pscustomobject]@{ value = @(
                    [pscustomobject]@{ id='open';  title='Open action';  maxScore=10; controlCategory='Identity'
                                       rank=1; deprecated=$false; remediation='Do the thing'
                                       controlStateUpdates=[pscustomobject]@{ state='Default' } }
                    [pscustomobject]@{ id='done';  title='Finished';     maxScore=5;  controlCategory='Identity'
                                       rank=2; deprecated=$false
                                       controlStateUpdates=[pscustomobject]@{ state='Default' } }
                    [pscustomobject]@{ id='dep';   title='Retired';      maxScore=7;  controlCategory='Device'
                                       rank=3; deprecated=$true
                                       controlStateUpdates=[pscustomobject]@{ state='Default' } }
                    [pscustomobject]@{ id='ign';   title='Dismissed';    maxScore=9;  controlCategory='Device'
                                       rank=4; deprecated=$false
                                       controlStateUpdates=[pscustomobject]@{ state='Ignored'; updatedBy='someone' } }
                    [pscustomobject]@{ id='unlic'; title='Not licensed'; maxScore=4;  controlCategory='Apps'
                                       rank=5; deprecated=$false
                                       controlStateUpdates=[pscustomobject]@{ state='Default' } }
                ) }
            }
            Mock Invoke-RestMethod -ParameterFilter { $Uri -match 'secureScores' -and $Uri -notmatch 'Profile' } -MockWith {
                [pscustomobject]@{ value = @(
                    [pscustomobject]@{
                        createdDateTime='2026-10-06T00:00:00Z'; currentScore=15; maxScore=35
                        controlScores=@(
                            [pscustomobject]@{ controlName='open'; controlCategory='Identity'; score=2; on='false'; scoreInPercentage=20 }
                            [pscustomobject]@{ controlName='done'; controlCategory='Identity'; score=5; on='true';  scoreInPercentage=100 }
                            [pscustomobject]@{ controlName='dep';  controlCategory='Device';   score=0; on='false' }
                            [pscustomobject]@{ controlName='ign';  controlCategory='Device';   score=0; on='false' }
                            [pscustomobject]@{ controlName='orphan'; controlCategory='Data';   score=3; on='true' })
                    }
                ) }
            }
        }
    }

    It 'treats the string "false" as $false, not as a truthy string' {
        $r = InModuleScope Msec {
            Get-MsecSecureScoreRecommendation -IncludeCompleted | Where-Object ControlName -eq 'open'
        }
        $r.Enabled | Should -BeFalse
        $r.Enabled | Should -BeOfType [bool]
    }

    It 'returns a scored control that has no profile, with a null Title rather than dropping it' {
        $r = InModuleScope Msec {
            Get-MsecSecureScoreRecommendation -IncludeCompleted | Where-Object ControlName -eq 'orphan'
        }
        $r | Should -Not -BeNullOrEmpty
        $r.Source | Should -Be 'ScoreOnly'
        $r.Title  | Should -BeNullOrEmpty
        # No profile means no maxScore, so points available is unknowable - not zero.
        $r.PointsAvailable | Should -BeNullOrEmpty
    }

    It 'gives a not-applicable control a NULL CurrentScore, never 0' {
        $r = InModuleScope Msec {
            Get-MsecSecureScoreRecommendation -IncludeNotApplicable -IncludeCompleted |
                Where-Object ControlName -eq 'unlic'
        }
        $r.Source       | Should -Be 'ProfileOnly'
        $r.CurrentScore | Should -BeNullOrEmpty
        $r.CurrentScore | Should -Not -Be 0
    }

    It 'excludes not-applicable controls unless asked' {
        $r = InModuleScope Msec {
            Get-MsecSecureScoreRecommendation -IncludeCompleted
        }
        @($r | Where-Object Source -eq 'ProfileOnly').Count | Should -Be 0
    }

    It 'excludes completed and deprecated controls by default' {
        $r = InModuleScope Msec {
            Get-MsecSecureScoreRecommendation
        }
        @($r | Where-Object ControlName -eq 'done').Count | Should -Be 0
        @($r | Where-Object ControlName -eq 'dep').Count  | Should -Be 0
    }

    It 'keeps an Ignored control visible and carries who ignored it' {
        $r = InModuleScope Msec {
            Get-MsecSecureScoreRecommendation | Where-Object ControlName -eq 'ign'
        }
        $r | Should -Not -BeNullOrEmpty
        $r.State          | Should -Be 'Ignored'
        $r.StateUpdatedBy | Should -Be 'someone'
        $r.PointsAvailable | Should -Be 9
    }

    It 'accepts a state the documentation does not list' {
        # ValidateSet would reject this; the live API returns AlternateMitigation.
        { InModuleScope Msec {
            Get-MsecSecureScoreRecommendation -State 'AlternateMitigation'
        } } | Should -Not -Throw
    }

    It 'computes PointsAvailable from the profile maxScore minus the achieved score' {
        $r = InModuleScope Msec {
            Get-MsecSecureScoreRecommendation | Where-Object ControlName -eq 'open'
        }
        $r.MaxScore        | Should -Be 10
        $r.CurrentScore    | Should -Be 2
        $r.PointsAvailable | Should -Be 8
    }
}