Public/Tests/Publish-IntuneAppPackage.Tests.ps1

BeforeAll {
    $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config'
    $env:TCS_SKIP_UPDATE_CHECK = '1'
    $env:TCS_TELEMETRY_OPTOUT = '1'
    $ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
    Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.intune.packaging.psd1') -Force
}

AfterAll {
    Remove-Module -Name tcs.intune.packaging -Force -ErrorAction SilentlyContinue
}
Describe 'Publish-IntuneAppPackage' {
    BeforeAll {
        . (Join-Path -Path $PSScriptRoot -ChildPath '../../../../tests/TestHelpers/New-TestIntuneWin.ps1')
        $script:Stubs = Add-GraphCommandStub
        $script:Package = (New-TestIntuneWin -Path (Join-Path -Path $TestDrive -ChildPath 'app.intunewin')).FullName
        $script:Json = Join-Path -Path $TestDrive -ChildPath 'app.json'
        @{
            ApplicationParameters = @{
                ApplicationName     = "O'Brien App"
                Description         = 'Desc'
                Publisher           = 'Contoso'
                Version             = '2.1'
                InstallCommand      = 'setup.exe /S'
                UninstallCommand    = 'setup.exe /U'
                InstallFor          = 'System'
                RestartBehavior     = 'suppress'
                DetectionRuleConfig = @{ '@odata.type' = '#microsoft.graph.win32LobAppFileSystemRule'; ruleType = 'detection'; path = 'C:\App'; fileOrFolderName = 'app.exe'; operationType = 'exists'; operator = 'notConfigured'; check32BitOn64System = $false }
            }
        } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:Json
    }

    AfterAll {
        foreach ($name in $script:Stubs) {
            Remove-Item -Path "Function:\global:$name" -ErrorAction SilentlyContinue
        }
    }

    BeforeEach {
        Mock -ModuleName tcs.intune.packaging Get-MgContext { [PSCustomObject]@{ Account = 'a'; TenantId = 't' } }
        Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest { [PSCustomObject]@{ value = @() } }
        Mock -ModuleName tcs.intune.packaging New-IntuneWin32Application { [PSCustomObject]@{ id = 'new-id' } }
        Mock -ModuleName tcs.intune.packaging Publish-IntuneWin32AppContent { '3' }
    }

    It 'Throws when not connected to Microsoft Graph' {
        Mock -ModuleName tcs.intune.packaging Get-MgContext { }
        { Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package } | Should -Throw '*Connect-MgGraph*'
    }

    It 'Creates a new app from the JSON configuration' {
        $app = Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails
        $app.id | Should -Be 'new-id'
        Should -Invoke -ModuleName tcs.intune.packaging Invoke-MgGraphRequest -Times 1 -Exactly -ParameterFilter {
            $Method -eq 'GET' -and $Uri -eq "v1.0/deviceAppManagement/mobileApps?`$filter=$([System.Uri]::EscapeDataString("displayName eq 'O''Brien App'"))"
        }
        Should -Invoke -ModuleName tcs.intune.packaging New-IntuneWin32Application -Times 1 -Exactly -ParameterFilter {
            $Name -eq "O'Brien App" -and $Publisher -eq 'Contoso' -and $InstallCommandLine -eq 'setup.exe /S' -and
            $UninstallCommandLine -eq 'setup.exe /U' -and $InstallExperienceRunAsAccount -eq 'system' -and
            $InstallExperienceDeviceRestartBehavior -eq 'suppress' -and $Version -eq [version]'2.1' -and
            $Rules.Count -eq 1 -and $Rules[0].fileOrFolderName -eq 'app.exe' -and $IntuneWinFilePath -eq $script:Package
        }
    }

    It 'Uses -Rules instead of the rules in the JSON file' {
        $rule = @{ '@odata.type' = '#microsoft.graph.win32LobAppProductCodeRule'; ruleType = 'detection'; productCode = '{X}' }
        $null = Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails -Rules $rule
        Should -Invoke -ModuleName tcs.intune.packaging New-IntuneWin32Application -Times 1 -Exactly -ParameterFilter { $Rules[0].productCode -eq '{X}' }
    }

    It 'Does not read the rules in the JSON file when -Rules is used' {
        $json = Join-Path -Path $TestDrive -ChildPath 'oldrules-override.json'
        @{ ApplicationParameters = @{ ApplicationName = 'App'; Description = 'D'; Publisher = 'P'; InstallCommand = 'i'; UninstallCommand = 'u'; DetectionRuleConfig = @{ Type = 'File' } } } | ConvertTo-Json -Depth 5 | Set-Content -Path $json
        $rule = @{ '@odata.type' = '#microsoft.graph.win32LobAppProductCodeRule'; ruleType = 'detection'; productCode = '{X}' }
        $null = Publish-IntuneAppPackage -IntuneAppJSONPath $json -IntuneWinPath $script:Package -NoTenantDetails -Rules $rule
        Should -Invoke -ModuleName tcs.intune.packaging New-IntuneWin32Application -Times 1 -Exactly -ParameterFilter { $Rules[0].productCode -eq '{X}' }
    }

    It 'Rejects rule configuration that was not made with New-IntuneWin32Rule' {
        $json = Join-Path -Path $TestDrive -ChildPath 'oldrules.json'
        @{ ApplicationParameters = @{ ApplicationName = 'App'; DetectionRuleConfig = @{ Type = 'File' } } } | ConvertTo-Json -Depth 5 | Set-Content -Path $json
        { Publish-IntuneAppPackage -IntuneAppJSONPath $json -IntuneWinPath $script:Package -NoTenantDetails } | Should -Throw '*New-IntuneWin32Rule*'
    }

    It 'Throws when the app exists and -Force is not used' {
        Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest { [PSCustomObject]@{ value = @([PSCustomObject]@{ '@odata.type' = '#microsoft.graph.win32LobApp'; id = 'existing'; displayName = "O'Brien App" }) } }
        { Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails } | Should -Throw '*already exists*'
        Should -Invoke -ModuleName tcs.intune.packaging Publish-IntuneWin32AppContent -Times 0 -Exactly
    }

    It 'Uploads a new content version to the existing app with -Force' {
        Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest { [PSCustomObject]@{ value = @([PSCustomObject]@{ '@odata.type' = '#microsoft.graph.win32LobApp'; id = 'existing'; displayName = "O'Brien App" }) } }
        $app = Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails -Force -Confirm:$false
        $app.id | Should -Be 'existing'
        $app.committedContentVersion | Should -Be '3'
        Should -Invoke -ModuleName tcs.intune.packaging Publish-IntuneWin32AppContent -Times 1 -Exactly -ParameterFilter { $AppId -eq 'existing' }
        Should -Invoke -ModuleName tcs.intune.packaging New-IntuneWin32Application -Times 0 -Exactly
    }

    It 'Uploads nothing with -WhatIf' {
        Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest { [PSCustomObject]@{ value = @([PSCustomObject]@{ '@odata.type' = '#microsoft.graph.win32LobApp'; id = 'existing' }) } }
        $null = Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails -Force -WhatIf
        Should -Invoke -ModuleName tcs.intune.packaging Publish-IntuneWin32AppContent -Times 0 -Exactly
    }

    Context 'Updating an existing app with -Force' {
        BeforeEach {
            $script:Calls = [System.Collections.Generic.List[object]]::new()
            Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest {
                $script:Calls.Add([PSCustomObject]@{ Method = $Method; Uri = $Uri; Body = $Body })
                if ($Method -eq 'GET') {
                    [PSCustomObject]@{ value = @([PSCustomObject]@{ '@odata.type' = '#microsoft.graph.win32LobApp'; id = 'existing'; displayName = "O'Brien App" }) }
                }
            }
            Mock -ModuleName tcs.intune.packaging Publish-IntuneWin32AppContent { $script:Calls.Add([PSCustomObject]@{ Method = 'CONTENT'; Uri = $AppId; Body = $null }); '4' }
        }

        It 'Updates the app properties from the JSON file after the new content is committed' {
            $app = Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails -Force -Confirm:$false
            $app.committedContentVersion | Should -Be '4'
            $methods = @($script:Calls | ForEach-Object Method)
            $methods | Should -Be @('GET', 'CONTENT', 'PATCH')
            $patch = $script:Calls | Where-Object Method -EQ 'PATCH'
            $patch.Uri | Should -Be 'v1.0/deviceAppManagement/mobileApps/existing'
            $body = $patch.Body | ConvertFrom-Json
            $body.'@odata.type' | Should -Be '#microsoft.graph.win32LobApp'
            $body.displayName | Should -Be "O'Brien App"
            $body.description | Should -Be 'Desc'
            $body.publisher | Should -Be 'Contoso'
            $body.installCommandLine | Should -Be 'setup.exe /S'
            $body.uninstallCommandLine | Should -Be 'setup.exe /U'
            $body.notes | Should -Match 'Version: 2\.1'
            $body.installExperience.runAsAccount | Should -Be 'system'
            $body.installExperience.deviceRestartBehavior | Should -Be 'suppress'
            @($body.rules).Count | Should -Be 1
            @($body.rules)[0].fileOrFolderName | Should -Be 'app.exe'
            $body.setupFilePath | Should -Be 'setup.exe'
            $body.fileName | Should -Be 'app.intunewin'
        }

        It 'Uses -Rules for the update' {
            $rule = @{ '@odata.type' = '#microsoft.graph.win32LobAppProductCodeRule'; ruleType = 'detection'; productCode = '{X}' }
            $null = Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails -Force -Confirm:$false -Rules $rule
            $body = ($script:Calls | Where-Object Method -EQ 'PATCH').Body | ConvertFrom-Json
            @($body.rules)[0].productCode | Should -Be '{X}'
        }
    }

    Context 'Assignments' {
        BeforeAll {
            $script:AssignJson = Join-Path -Path $TestDrive -ChildPath 'assign.json'
            @{
                ApplicationParameters = @{
                    ApplicationName     = 'Assigned App'
                    Description         = 'Desc'
                    Publisher           = 'Contoso'
                    InstallCommand      = 'setup.exe /S'
                    UninstallCommand    = 'setup.exe /U'
                    AssignmentType      = 'User-Group'
                    AssignmentGroup     = 'Intune-AG-App-Available'
                    AssignmentIntent    = 'available'
                    FilterRuleType      = 'Include'
                    FilterRule          = 'Corporate devices'
                    DetectionRuleConfig = @{ '@odata.type' = '#microsoft.graph.win32LobAppFileSystemRule'; ruleType = 'detection'; path = 'C:\App'; fileOrFolderName = 'app.exe'; operationType = 'exists'; operator = 'notConfigured'; check32BitOn64System = $false }
                }
            } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:AssignJson
        }

        BeforeEach {
            Mock -ModuleName tcs.intune.packaging Add-IntuneWin32AppAssignment { [ordered]@{ intent = $Intent } }
        }

        It 'Assigns the new app as set in the JSON file' {
            $app = Publish-IntuneAppPackage -IntuneAppJSONPath $script:AssignJson -IntuneWinPath $script:Package -NoTenantDetails -Confirm:$false
            Should -Invoke -ModuleName tcs.intune.packaging Add-IntuneWin32AppAssignment -Times 1 -Exactly -ParameterFilter {
                $AppId -eq 'new-id' -and $AssignmentType -eq 'User-Group' -and $AssignmentGroup -eq 'Intune-AG-App-Available' -and
                $Intent -eq 'available' -and $FilterRuleType -eq 'Include' -and $FilterRule -eq 'Corporate devices'
            }
            $app.Assignment.intent | Should -Be 'available'
        }

        It 'Lets parameters override the JSON file' {
            $null = Publish-IntuneAppPackage -IntuneAppJSONPath $script:AssignJson -IntuneWinPath $script:Package -NoTenantDetails -Confirm:$false -AssignmentType All-Devices -AssignmentGroup '' -AssignmentIntent required -FilterRuleType Exclude -FilterRule 'Other'
            Should -Invoke -ModuleName tcs.intune.packaging Add-IntuneWin32AppAssignment -Times 1 -Exactly -ParameterFilter {
                $AssignmentType -eq 'All-Devices' -and -not $AssignmentGroup -and $Intent -eq 'required' -and $FilterRuleType -eq 'Exclude' -and $FilterRule -eq 'Other'
            }
        }

        It 'Assigns an existing app updated with -Force' {
            Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest { [PSCustomObject]@{ value = @([PSCustomObject]@{ '@odata.type' = '#microsoft.graph.win32LobApp'; id = 'existing'; displayName = 'Assigned App' }) } }
            $null = Publish-IntuneAppPackage -IntuneAppJSONPath $script:AssignJson -IntuneWinPath $script:Package -NoTenantDetails -Force -Confirm:$false
            Should -Invoke -ModuleName tcs.intune.packaging Add-IntuneWin32AppAssignment -Times 1 -Exactly -ParameterFilter { $AppId -eq 'existing' }
        }

        It 'Does not assign with -NoAssignment or without an assignment type' {
            $null = Publish-IntuneAppPackage -IntuneAppJSONPath $script:AssignJson -IntuneWinPath $script:Package -NoTenantDetails -NoAssignment -Confirm:$false
            $null = Publish-IntuneAppPackage -IntuneAppJSONPath $script:Json -IntuneWinPath $script:Package -NoTenantDetails -Confirm:$false
            Should -Invoke -ModuleName tcs.intune.packaging Add-IntuneWin32AppAssignment -Times 0 -Exactly
        }

        It 'Checks the assignment settings before creating the app' {
            { Publish-IntuneAppPackage -IntuneAppJSONPath $script:AssignJson -IntuneWinPath $script:Package -NoTenantDetails -Confirm:$false -AssignmentType Device-Group -AssignmentGroup '' } | Should -Throw '*needs AssignmentGroup*'
            Should -Invoke -ModuleName tcs.intune.packaging New-IntuneWin32Application -Times 0 -Exactly
            Should -Invoke -ModuleName tcs.intune.packaging Add-IntuneWin32AppAssignment -Times 0 -Exactly
        }
    }
}