Tests/Get-MsecIntuneDevice.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecIntuneDevice. The function GETs /deviceManagement/managedDevices
# with a $select filter and projects each device row to a flat PSCustomObject.
# Tests cover:
# - Field projection (Graph camelCase -> output PascalCase).
# - The 9999-sentinel for ComplianceGraceUntil becomes $null.
# - DateTime strings are converted to [datetime] (so callers can compare with
# (Get-Date).AddDays(...) without manual parsing).
# - 403 is rewritten to mention DeviceManagementManagedDevices.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-MsecIntuneDevice' {
    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 'projects every Graph row to the documented flat shape, with DateTime conversion and grace-sentinel nullification' {
        $rows = 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 '/deviceManagement/managedDevices' } -MockWith {
                [pscustomobject]@{ value = @(
                    [pscustomobject]@{
                        id = 'd-1'; deviceName = 'lap-01'
                        userPrincipalName = 'admin@contoso.com'; userDisplayName = 'Admin'
                        operatingSystem = 'Windows'; osVersion = '10.0.26100.5074'
                        model = 'XPS 13'; manufacturer = 'Dell'
                        complianceState = 'compliant'
                        # 9999 sentinel = no grace period -> ComplianceGraceUntil must be $null.
                        complianceGracePeriodExpirationDateTime = '9999-12-31T23:59:59.9999999Z'
                        managementState = 'managed'; managementAgent = 'mdm'
                        managedDeviceOwnerType = 'company'
                        isEncrypted = $true; jailBroken = 'False'; azureADRegistered = $true
                        enrolledDateTime = '2025-09-01T10:00:00Z'
                        lastSyncDateTime = '2026-06-07T15:30:00Z'
                        serialNumber = 'SN-001'
                    }
                    [pscustomobject]@{
                        id = 'd-2'; deviceName = 'phone-01'
                        userPrincipalName = 'admin@contoso.com'; userDisplayName = 'Admin'
                        operatingSystem = 'iOS'; osVersion = '18.5'
                        model = 'iPhone 16 Pro'; manufacturer = 'Apple'
                        # In-grace device: grace expiration is a REAL future date, not the sentinel.
                        complianceState = 'inGracePeriod'
                        complianceGracePeriodExpirationDateTime = '2026-06-15T00:00:00Z'
                        managementState = 'managed'; managementAgent = 'mdm'
                        managedDeviceOwnerType = 'personal'
                        isEncrypted = $true; jailBroken = 'False'; azureADRegistered = $true
                        enrolledDateTime = '2026-01-10T12:00:00Z'
                        lastSyncDateTime = '2026-06-08T11:00:00Z'
                        serialNumber = 'SN-002'
                    }
                ) }
            }

            Get-MsecIntuneDevice
        }

        $rows.Count | Should -Be 2

        # Row 1: Windows laptop, fully compliant, sentinel grace.
        $win = $rows | Where-Object Id -eq 'd-1'
        $win.DeviceName        | Should -Be 'lap-01'
        $win.Os                | Should -Be 'Windows'
        $win.ComplianceState   | Should -Be 'compliant'
        $win.Manufacturer      | Should -Be 'Dell'
        $win.Ownership         | Should -Be 'company'
        $win.IsEncrypted       | Should -BeTrue
        $win.ComplianceGraceUntil | Should -BeNullOrEmpty   # 9999 sentinel -> null
        $win.EnrolledDateTime  | Should -BeOfType [datetime]
        $win.LastSyncDateTime  | Should -BeOfType [datetime]

        # Row 2: in-grace, real future grace date.
        $ios = $rows | Where-Object Id -eq 'd-2'
        $ios.Os                | Should -Be 'iOS'
        $ios.ComplianceState   | Should -Be 'inGracePeriod'
        $ios.Ownership         | Should -Be 'personal'
        $ios.ComplianceGraceUntil | Should -BeOfType [datetime]
        $ios.ComplianceGraceUntil | Should -Be ([datetime]'2026-06-15T00:00:00Z')
    }

    It 'sends a $select trimming the request to the documented columns (so we do not pull all 80+ fields)' {
        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:CapturedUri = $null
            Mock Invoke-RestMethod -ParameterFilter { $Uri -match '/deviceManagement/managedDevices' } -MockWith {
                $script:CapturedUri = $Uri
                [pscustomobject]@{ value = @() }
            }

            Get-MsecIntuneDevice | Out-Null

            $script:CapturedUri | Should -Match '\$select='
            # Spot-check that key documented columns are in the $select.
            $script:CapturedUri | Should -Match 'complianceState'
            $script:CapturedUri | Should -Match 'lastSyncDateTime'
            $script:CapturedUri | Should -Match 'azureADRegistered'
        }
    }

    It 'rewrites a 403 to mention the missing DeviceManagementManagedDevices.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 '/deviceManagement/managedDevices' } -MockWith {
                throw 'Response status code does not indicate success: 403 (Forbidden).'
            }

            { Get-MsecIntuneDevice } |
                Should -Throw -ExpectedMessage '*DeviceManagementManagedDevices.Read.All*'
        }
    }
}