Public/Tests/New-IntuneApplication.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 'New-IntuneApplication' {
    BeforeEach {
        $Source = Join-Path -Path $TestDrive -ChildPath ([guid]::NewGuid().ToString())
        $null = New-Item -Path $Source -ItemType Directory
        Set-Content -Path (Join-Path -Path $Source -ChildPath 'setup.exe') -Value 'x'
        Set-Content -Path (Join-Path -Path $Source -ChildPath 'config.xml') -Value 'x'
        $Output = Join-Path -Path $TestDrive -ChildPath ([guid]::NewGuid().ToString())
        $null = New-Item -Path $Output -ItemType Directory
        $Common = @{
            ApplicationName       = 'MyApp'
            MainInstallerFileName = 'setup.exe'
            OutputFolder          = $Output
            InstallCommand        = 'setup.exe /S'
            UninstallCommand      = 'setup.exe /U'
            DetectionRuleConfig   = @{ Type = 'File' }
            AssignmentType        = 'All-Devices'
            Publisher             = 'Contoso'
        }
        # Never download IntuneWinAppUtil.exe in tests
        Mock -ModuleName tcs.intune.packaging Get-IntunePackagingTool { throw 'No downloads in tests.' }
        Mock -ModuleName tcs.intune.packaging Confirm-ToolDownload { $false }
    }

    It 'Writes the JSON file with the default description filled in' {
        $result = New-IntuneApplication @Common -SourceFiles $Source -Version '2.0' -NoIntuneWin
        $result.JsonPath | Should -Be (Join-Path -Path $Output -ChildPath 'MyApp.2.0.json')
        $result.IntuneWinPath | Should -BeNullOrEmpty
        $json = Get-Content -Path $result.JsonPath -Raw | ConvertFrom-Json
        $json.ApplicationParameters.Publisher | Should -Be 'Contoso'
        $json.ApplicationParameters.Description | Should -Match 'Publisher: Contoso'
        $json.ApplicationParameters.Description | Should -Match 'Version: 2.0'
        $json.IntuneWinParameters.MainInstallerFileName | Should -Be 'setup.exe'
    }

    It 'Refuses to overwrite the JSON file without -Overwrite' {
        $null = New-IntuneApplication @Common -SourceFiles $Source -NoIntuneWin
        { New-IntuneApplication @Common -SourceFiles $Source -NoIntuneWin } | Should -Throw '*-Overwrite*'
        { New-IntuneApplication @Common -SourceFiles $Source -NoIntuneWin -Overwrite } | Should -Not -Throw
    }

    It 'Throws when the main installer is not one of several source files' {
        $files = Join-Path -Path $Source -ChildPath 'config.xml'
        { New-IntuneApplication @Common -SourceFiles $files -NoIntuneWin } | Should -Throw '*does not exist in the SourceFiles*'
        $Common.MainInstallerFileName = 'other.exe'
        { New-IntuneApplication @Common -SourceFiles (Join-Path -Path $Source -ChildPath 'setup.exe'), $files -NoIntuneWin } | Should -Throw '*does not exist in the SourceFiles*'
    }

    It 'Throws when a source file does not exist' {
        { New-IntuneApplication @Common -SourceFiles (Join-Path -Path $TestDrive -ChildPath 'nope') -NoIntuneWin } | Should -Throw '*does not exist*'
    }

    It 'Runs IntuneWinAppUtil.exe with the source folder and main installer' {
        $tool = Join-Path -Path $TestDrive -ChildPath 'IntuneWinAppUtil.exe'
        Set-Content -Path $tool -Value 'x'
        Mock -ModuleName tcs.intune.packaging Invoke-Executable {
            Set-Content -Path (Join-Path -Path $Output -ChildPath 'setup.intunewin') -Value 'pkg'
            [PSCustomObject]@{ ExitCode = 0; StandardOutput = ''; StandardError = '' }
        }
        $result = New-IntuneApplication @Common -SourceFiles $Source -NoJson -IntuneToolsPath $tool
        $result.IntuneWinPath | Should -Be (Join-Path -Path $Output -ChildPath 'setup.intunewin')
        Should -Invoke -ModuleName tcs.intune.packaging Invoke-Executable -Times 1 -Exactly -ParameterFilter {
            $FilePath -eq $tool -and $Arguments -like "*-c `"$Source`"*" -and $Arguments -like '*setup.exe*'
        }
    }

    It 'Publishes the package and removes the files unless -NoCleanUp is used' {
        $tool = Join-Path -Path $TestDrive -ChildPath 'IntuneWinAppUtil.exe'
        Set-Content -Path $tool -Value 'x'
        Mock -ModuleName tcs.intune.packaging Invoke-Executable {
            Set-Content -Path (Join-Path -Path $Output -ChildPath 'setup.intunewin') -Value 'pkg'
            [PSCustomObject]@{ ExitCode = 0 }
        }
        Mock -ModuleName tcs.intune.packaging Publish-IntuneAppPackage { [PSCustomObject]@{ id = 'app-1' } }
        $result = New-IntuneApplication @Common -SourceFiles $Source -IntuneToolsPath $tool -Publish -Confirm:$false
        $result.App.id | Should -Be 'app-1'
        Should -Invoke -ModuleName tcs.intune.packaging Publish-IntuneAppPackage -Times 1 -Exactly -ParameterFilter {
            $IntuneAppJSONPath -eq (Join-Path -Path $Output -ChildPath 'MyApp.1.0.json') -and $IntuneWinPath -eq (Join-Path -Path $Output -ChildPath 'setup.intunewin')
        }
        Join-Path -Path $Output -ChildPath 'MyApp.1.0.json' | Should -Not -Exist
        Join-Path -Path $Output -ChildPath 'setup.intunewin' | Should -Not -Exist

        $kept = New-IntuneApplication @Common -SourceFiles $Source -IntuneToolsPath $tool -Publish -NoCleanUp -Confirm:$false
        $kept.JsonPath | Should -Exist
    }

    It 'Refuses -Publish without the JSON file or package before building anything' {
        { New-IntuneApplication @Common -SourceFiles $Source -NoIntuneWin -Publish } | Should -Throw '*-NoJson or -NoIntuneWin*'
        { New-IntuneApplication @Common -SourceFiles $Source -NoJson -Publish } | Should -Throw '*-NoJson or -NoIntuneWin*'
        @(Get-ChildItem -LiteralPath $Output).Count | Should -Be 0
    }

    It 'Stages several source files and folders with their contents in a temporary folder that is removed' {
        $tool = Join-Path -Path $TestDrive -ChildPath 'IntuneWinAppUtil.exe'
        Set-Content -Path $tool -Value 'x'
        $data = Join-Path -Path $Source -ChildPath 'data'
        $null = New-Item -Path (Join-Path -Path $data -ChildPath 'nested') -ItemType Directory -Force
        Set-Content -Path (Join-Path -Path $data -ChildPath 'nested/inner.txt') -Value 'x'
        $script:Staged = $null
        Mock -ModuleName tcs.intune.packaging Invoke-Executable {
            $staging = [regex]::Match($Arguments, '-c "([^"]+)"').Groups[1].Value
            $script:Staged = [PSCustomObject]@{
                Folder = $staging
                Inner  = Test-Path -LiteralPath (Join-Path -Path $staging -ChildPath 'data/nested/inner.txt')
                Setup  = Test-Path -LiteralPath (Join-Path -Path $staging -ChildPath 'setup.exe')
            }
            Set-Content -Path (Join-Path -Path $Output -ChildPath 'setup.intunewin') -Value 'pkg'
            [PSCustomObject]@{ ExitCode = 0 }
        }
        $result = New-IntuneApplication @Common -SourceFiles (Join-Path -Path $Source -ChildPath 'setup.exe'), $data -IntuneToolsPath $tool -NoJson
        $result.IntuneWinPath | Should -Be (Join-Path -Path $Output -ChildPath 'setup.intunewin')
        $script:Staged.Inner | Should -BeTrue
        $script:Staged.Setup | Should -BeTrue
        $script:Staged.Folder | Should -Not -BeLike "$Output*"
        Test-Path -LiteralPath $script:Staged.Folder | Should -BeFalse
        @(Get-ChildItem -LiteralPath $Output).Name | Should -Be @('setup.intunewin')
    }

    It 'Removes the staging folder when packaging fails' {
        $tool = Join-Path -Path $TestDrive -ChildPath 'IntuneWinAppUtil.exe'
        Set-Content -Path $tool -Value 'x'
        $script:StagingFolder = $null
        Mock -ModuleName tcs.intune.packaging Invoke-Executable {
            $script:StagingFolder = [regex]::Match($Arguments, '-c "([^"]+)"').Groups[1].Value
            [PSCustomObject]@{ ExitCode = 1; StandardError = 'failed' }
        }
        { New-IntuneApplication @Common -SourceFiles (Join-Path -Path $Source -ChildPath 'setup.exe'), (Join-Path -Path $Source -ChildPath 'config.xml') -IntuneToolsPath $tool -NoJson } | Should -Throw '*exit code 1*'
        $script:StagingFolder | Should -Not -BeNullOrEmpty
        Test-Path -LiteralPath $script:StagingFolder | Should -BeFalse
    }

    It 'Writes the assignment settings, but not the command options, to the JSON file' {
        $Common.AssignmentType = 'User-Group'
        $result = New-IntuneApplication @Common -SourceFiles $Source -NoIntuneWin -AssignmentGroup 'Intune-AG-MyApp-Available' -AssignmentIntent available -FilterRuleType Include -FilterRule 'Corporate'
        $json = (Get-Content -Path $result.JsonPath -Raw | ConvertFrom-Json).ApplicationParameters
        $json.AssignmentType | Should -Be 'User-Group'
        $json.AssignmentGroup | Should -Be 'Intune-AG-MyApp-Available'
        $json.AssignmentIntent | Should -Be 'available'
        $json.FilterRuleType | Should -Be 'Include'
        $json.FilterRule | Should -Be 'Corporate'
        $json.PSObject.Properties.Name | Should -Not -Contain 'NoIntuneWin'
    }

    It 'Checks the assignment settings for -Publish before building anything' {
        $Common.AssignmentType = 'Device-Group'
        { New-IntuneApplication @Common -SourceFiles $Source -Publish } | Should -Throw '*needs AssignmentGroup*'
        @(Get-ChildItem -LiteralPath $Output).Count | Should -Be 0
    }

    It 'Rejects an application name that is not a valid file name' {
        $Common.ApplicationName = '../MyApp'
        { New-IntuneApplication @Common -SourceFiles $Source -NoIntuneWin } | Should -Throw '*cannot be used in a file or folder name*'
    }
}