tests/BackupModules.Tests.ps1

#Requires -Version 7.0
<#
.SYNOPSIS
Tests for backup modules

.DESCRIPTION
Pester tests for the IntuneBackup modules subset
#>


BeforeAll {
    Import-Module "$PSScriptRoot/../IntuneBackup.psm1" -Force
    $script:token = ConvertTo-SecureString -String 'test-token' -AsPlainText -Force
}

Describe "Backup-DeviceManagementSettings" {
    It "should handle singleton response with preset filename" {
        # Mock Invoke-GraphRequest2 to return a single object (not an array)
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/settings' } {
            return @{
                id = 'test-id'
                someProperty = 'someValue'
            }
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup {
            # Verify that Save-BackupItem was called with the correct filename
            if ($FileName -ne 'settings') {
                throw "Expected filename 'settings', got '$FileName'"
            }
        }

        Backup-DeviceManagementSettings -BackupPath 'TestDrive:\backup' -Token $script:token

        Assert-MockCalled Save-BackupItem -ModuleName IntuneBackup -Times 1
    }

    It "should create the correct folder path" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/settings' } {
            return @{ id = 'test-id' }
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup {
            $expectedPath = Join-Path 'TestDrive:\backup' 'Device Management Settings'
            if ($Folder -ne $expectedPath) {
                throw "Expected folder '$expectedPath', got '$Folder'"
            }
        }

        Backup-DeviceManagementSettings -BackupPath 'TestDrive:\backup' -Token $script:token

        Assert-MockCalled Save-BackupItem -ModuleName IntuneBackup -Times 1
    }
}

Describe "Backup-Filters" {
    It "should handle list response with multiple items" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/assignmentFilters' } {
            return @(
                @{ id = 'filter-1'; displayName = 'Filter 1' },
                @{ id = 'filter-2'; displayName = 'Filter 2' }
            )
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup

        Backup-Filters -BackupPath 'TestDrive:\backup' -Token $script:token

        Assert-MockCalled Save-BackupItem -ModuleName IntuneBackup -Times 2
    }

    It "should not fetch assignments" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/assignmentFilters' } {
            return @( @{ id = 'filter-1' } )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -like '*assignments' } {
            throw "Should not fetch assignments for filters"
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup

        Backup-Filters -BackupPath 'TestDrive:\backup' -Token $script:token

        # Verify Invoke-GraphRequest2 was only called once (for the main URI)
        Assert-MockCalled Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/assignmentFilters' } -Times 1
    }
}

Describe "Backup-PowerShellScripts" {
    It "should decode base64 script content and save to Script Data folder" {
        $scriptContent = 'Write-Host "Hello"'
        $base64Content = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($scriptContent))

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/deviceManagementScripts/' } {
            return @( @{ id = 'script-1' } )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -like '*/deviceManagementScripts/*' -and $Uri -notlike '*assignments' } {
            return @{
                id = 'script-1'
                fileName = 'test-script.ps1'
                displayName = 'Test Script'
                scriptContent = $base64Content
            }
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -like '*assignments' } {
            return $null
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup
        Mock Set-Content -ModuleName IntuneBackup
        Mock New-Item -ModuleName IntuneBackup

        Backup-PowerShellScripts -BackupPath 'TestDrive:\backup' -Token $script:token

        # Verify Set-Content was called with decoded content
        Assert-MockCalled Set-Content -ModuleName IntuneBackup -Times 1
    }

    It "should fetch assignments" {
        $base64Content = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('test'))

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/deviceManagementScripts/' } {
            return @( @{ id = 'script-1' } )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -like '*/deviceManagementScripts/*' -and $Uri -notlike '*assignments' } {
            return @{
                id = 'script-1'
                fileName = 'test.ps1'
                scriptContent = $base64Content
            }
        }

        Mock Resolve-Assignments -ModuleName IntuneBackup {
            return @( @{ id = 'assignment-1' } )
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup
        Mock Set-Content
        Mock New-Item -ParameterFilter { $ItemType -eq 'Directory' }

        Backup-PowerShellScripts -BackupPath 'TestDrive:\backup' -Token $script:token

        Assert-MockCalled Resolve-Assignments -ModuleName IntuneBackup -Times 1
    }
}

Describe "Backup-CompliancePartner" {
    It "should filter out partners with unknown state" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/deviceManagement/complianceManagementPartners' } {
            return @(
                @{ id = 'partner-1'; displayName = 'Partner 1'; partnerState = 'active' },
                @{ id = 'partner-2'; displayName = 'Partner 2'; partnerState = 'unknown' },
                @{ id = 'partner-3'; displayName = 'Partner 3'; partnerState = 'inactive' }
            )
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup

        Backup-CompliancePartner -BackupPath 'TestDrive:\backup' -Token $script:token

        # Should save only 2 items (active and inactive, not unknown)
        Assert-MockCalled Save-BackupItem -ModuleName IntuneBackup -Times 2
    }
}

Describe "Build-AssignmentReport" {
    It "saves report.json inside 'Assignment Report' folder" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup {
            # Return empty list for all category list calls; handled by URI matching below
            return @()
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceHealthScripts' } {
            return @(
                [PSCustomObject]@{ id = 'pr-1'; displayName = 'My Remediation'; '@odata.type' = $null }
            )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceHealthScripts/pr-1/assignments' } {
            return @(
                [PSCustomObject]@{
                    intent = ''
                    target = [PSCustomObject]@{
                        '@odata.type' = '#microsoft.graph.groupAssignmentTarget'
                        groupId       = 'grp-1'
                    }
                }
            )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/groups/grp-1*' } {
            return [PSCustomObject]@{
                id             = 'grp-1'
                displayName    = 'TestGroup'
                groupTypes     = @()
                membershipRule = $null
            }
        }

        $testFolder = Join-Path ([System.IO.Path]::GetTempPath()) "IntuneReport_$(Get-Random)"
        try {
            Build-AssignmentReport -BackupPath $testFolder -Token $script:token

            $reportPath = Join-Path $testFolder 'Assignment Report' 'report.json'
            Test-Path $reportPath | Should -Be $true

            $report = Get-Content $reportPath -Raw | ConvertFrom-Json
            $report | Should -Not -BeNullOrEmpty
            $report[0].groupName | Should -Be 'TestGroup'
            $report[0].groupType | Should -Be 'StaticMembership'
            $report[0].assignedTo.'Proactive Remediations'[0].name | Should -Be 'My Remediation'
        }
        finally {
            Remove-Item $testFolder -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It "produces DynamicMembership when groupTypes contains 'DynamicMembership'" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup { return @() }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceHealthScripts' } {
            return @( [PSCustomObject]@{ id = 'pr-2'; displayName = 'DynTest'; '@odata.type' = $null } )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceHealthScripts/pr-2/assignments' } {
            return @(
                [PSCustomObject]@{
                    intent = ''
                    target = [PSCustomObject]@{
                        '@odata.type' = '#microsoft.graph.groupAssignmentTarget'
                        groupId       = 'grp-dyn'
                    }
                }
            )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/groups/grp-dyn*' } {
            return [PSCustomObject]@{
                id             = 'grp-dyn'
                displayName    = 'DynGroup'
                groupTypes     = @('DynamicMembership')
                membershipRule = 'device.os -eq "Windows"'
            }
        }

        $testFolder = Join-Path ([System.IO.Path]::GetTempPath()) "IntuneReport_$(Get-Random)"
        try {
            Build-AssignmentReport -BackupPath $testFolder -Token $script:token

            $report = Get-Content (Join-Path $testFolder 'Assignment Report' 'report.json') -Raw | ConvertFrom-Json
            $report[0].groupType      | Should -Be 'DynamicMembership'
            $report[0].membershipRule | Should -Be 'device.os -eq "Windows"'
        }
        finally {
            Remove-Item $testFolder -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It "sets intent to 'apply' for Device Configurations" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup { return @() }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceConfigurations' } {
            return @( [PSCustomObject]@{ id = 'dc-1'; displayName = 'MyCfg'; '@odata.type' = '#microsoft.graph.windows10GeneralConfiguration' } )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceConfigurations/dc-1/assignments' } {
            return @(
                [PSCustomObject]@{
                    intent = $null
                    target = [PSCustomObject]@{
                        '@odata.type' = '#microsoft.graph.groupAssignmentTarget'
                        groupId       = 'grp-dc'
                    }
                }
            )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/groups/grp-dc*' } {
            return [PSCustomObject]@{ id = 'grp-dc'; displayName = 'DCGroup'; groupTypes = @(); membershipRule = $null }
        }

        $testFolder = Join-Path ([System.IO.Path]::GetTempPath()) "IntuneReport_$(Get-Random)"
        try {
            Build-AssignmentReport -BackupPath $testFolder -Token $script:token

            $report = Get-Content (Join-Path $testFolder 'Assignment Report' 'report.json') -Raw | ConvertFrom-Json
            $report[0].assignedTo.'Device Configurations'[0].intent | Should -Be 'apply'
            $report[0].assignedTo.'Device Configurations'[0].type   | Should -Be 'windows10GeneralConfiguration'
        }
        finally {
            Remove-Item $testFolder -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It "reads intent from assignment for Applications" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup { return @() }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/mobileApps' } {
            return @( [PSCustomObject]@{ id = 'app-1'; displayName = 'MyApp'; '@odata.type' = '#microsoft.graph.win32LobApp' } )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/mobileApps/app-1/assignments' } {
            return @(
                [PSCustomObject]@{
                    intent = 'required'
                    target = [PSCustomObject]@{
                        '@odata.type' = '#microsoft.graph.groupAssignmentTarget'
                        groupId       = 'grp-app'
                    }
                }
            )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/groups/grp-app*' } {
            return [PSCustomObject]@{ id = 'grp-app'; displayName = 'AppGroup'; groupTypes = @(); membershipRule = $null }
        }

        $testFolder = Join-Path ([System.IO.Path]::GetTempPath()) "IntuneReport_$(Get-Random)"
        try {
            Build-AssignmentReport -BackupPath $testFolder -Token $script:token

            $report = Get-Content (Join-Path $testFolder 'Assignment Report' 'report.json') -Raw | ConvertFrom-Json
            $report[0].assignedTo.'Applications'[0].intent | Should -Be 'required'
        }
        finally {
            Remove-Item $testFolder -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It "skips allDevices / allLicensedUsers targets" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup { return @() }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceHealthScripts' } {
            return @( [PSCustomObject]@{ id = 'pr-all'; displayName = 'AllDevices'; '@odata.type' = $null } )
        }

        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup `
            -ParameterFilter { $Uri -like '*/deviceHealthScripts/pr-all/assignments' } {
            return @(
                [PSCustomObject]@{
                    intent = ''
                    target = [PSCustomObject]@{
                        '@odata.type' = '#microsoft.graph.allDevicesAssignmentTarget'
                        groupId       = $null
                    }
                }
            )
        }

        $testFolder = Join-Path ([System.IO.Path]::GetTempPath()) "IntuneReport_$(Get-Random)"
        try {
            Build-AssignmentReport -BackupPath $testFolder -Token $script:token

            $reportPath = Join-Path $testFolder 'Assignment Report' 'report.json'
            # Report should not be written (no group assignments)
            Test-Path $reportPath | Should -Be $false
        }
        finally {
            Remove-Item $testFolder -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

Describe "Backup-ConditionalAccess" {
    It "should remove authenticationStrength@odata.context from grantControls" {
        Mock Invoke-GraphRequest2 -ModuleName IntuneBackup -ParameterFilter { $Uri -eq '/beta/identity/conditionalAccess/policies' } {
            return @(
                [PSCustomObject]@{
                    id = 'ca-1'
                    displayName = 'CA Policy 1'
                    grantControls = [PSCustomObject]@{
                        'authenticationStrength@odata.context' = 'http://example.com'
                        operator = 'AND'
                    }
                }
            )
        }

        Mock Remove-VolatileKeys -ModuleName IntuneBackup {
            return $InputObject
        }

        Mock Save-BackupItem -ModuleName IntuneBackup {
            # Verify the property was removed
            if ($Item.grantControls.'authenticationStrength@odata.context') {
                throw "authenticationStrength@odata.context should have been removed"
            }
        }

        Backup-ConditionalAccess -BackupPath 'TestDrive:\backup' -Token $script:token

        Assert-MockCalled Save-BackupItem -ModuleName IntuneBackup -Times 1
    }
}