Private/Tests/IntuneAssignment.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
    . (Join-Path -Path $PSScriptRoot -ChildPath '../../../../tests/TestHelpers/New-TestIntuneWin.ps1')
    $script:Stubs = Add-GraphCommandStub
}

AfterAll {
    foreach ($name in $script:Stubs) {
        Remove-Item -Path "Function:\global:$name" -ErrorAction SilentlyContinue
    }
    Remove-Module -Name tcs.intune.packaging -Force -ErrorAction SilentlyContinue
}

Describe 'Add-IntuneWin32AppAssignment' {
    BeforeEach {
        $script:Posts = [System.Collections.Generic.List[object]]::new()
        $script:Gets = [System.Collections.Generic.List[string]]::new()
        $script:ExistingAssignments = @()
        $script:Groups = @([PSCustomObject]@{ id = 'group-1'; displayName = 'Intune-AG-App-Required' })
        $script:Filters = @(
            [PSCustomObject]@{ id = 'filter-1'; displayName = 'Corporate devices' }
            [PSCustomObject]@{ id = 'filter-2'; displayName = 'Other' }
        )
        Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest {
            if ($Method -eq 'POST') {
                $script:Posts.Add([PSCustomObject]@{ Uri = $Uri; Body = ($Body | ConvertFrom-Json) })
                return
            }
            $script:Gets.Add($Uri)
            switch -Wildcard ($Uri) {
                'v1.0/groups*' { [PSCustomObject]@{ value = $script:Groups } }
                'beta/deviceManagement/assignmentFilters*' { [PSCustomObject]@{ value = $script:Filters } }
                '*/assignments' { [PSCustomObject]@{ value = $script:ExistingAssignments } }
            }
        }
    }

    It 'Assigns to all devices as required with Win32 assignment settings' {
        InModuleScope tcs.intune.packaging { $null = Add-IntuneWin32AppAssignment -AppId 'app-1' -AssignmentType All-Devices }
        $script:Posts.Count | Should -Be 1
        $script:Posts[0].Uri | Should -Be 'beta/deviceAppManagement/mobileApps/app-1/assign'
        $assignment = @($script:Posts[0].Body.mobileAppAssignments)
        $assignment.Count | Should -Be 1
        $assignment[0].'@odata.type' | Should -Be '#microsoft.graph.mobileAppAssignment'
        $assignment[0].intent | Should -Be 'required'
        $assignment[0].target.'@odata.type' | Should -Be '#microsoft.graph.allDevicesAssignmentTarget'
        $assignment[0].settings.'@odata.type' | Should -Be '#microsoft.graph.win32LobAppAssignmentSettings'
        @($script:Gets | Where-Object { $_ -like 'v1.0/groups*' }).Count | Should -Be 0
    }

    It 'Assigns to all users with the chosen intent' {
        InModuleScope tcs.intune.packaging { $null = Add-IntuneWin32AppAssignment -AppId 'app-1' -AssignmentType All-Users -Intent available }
        $assignment = @($script:Posts[0].Body.mobileAppAssignments)[0]
        $assignment.intent | Should -Be 'available'
        $assignment.target.'@odata.type' | Should -Be '#microsoft.graph.allLicensedUsersAssignmentTarget'
    }

    It 'Looks up a group by display name' {
        InModuleScope tcs.intune.packaging { $null = Add-IntuneWin32AppAssignment -AppId 'app-1' -AssignmentType Device-Group -AssignmentGroup "Intune-AG-App-Required" -Intent uninstall }
        $script:Gets | Where-Object { $_ -like 'v1.0/groups*' } | Should -Be "v1.0/groups?`$filter=$([System.Uri]::EscapeDataString("displayName eq 'Intune-AG-App-Required'"))&`$select=id,displayName"
        $assignment = @($script:Posts[0].Body.mobileAppAssignments)[0]
        $assignment.intent | Should -Be 'uninstall'
        $assignment.target.'@odata.type' | Should -Be '#microsoft.graph.groupAssignmentTarget'
        $assignment.target.groupId | Should -Be 'group-1'
    }

    It 'Uses a group ID without a lookup' {
        $groupId = [guid]::NewGuid().ToString()
        InModuleScope tcs.intune.packaging -Parameters @{ GroupId = $groupId } { $null = Add-IntuneWin32AppAssignment -AppId 'app-1' -AssignmentType User-Group -AssignmentGroup $GroupId }
        @($script:Gets | Where-Object { $_ -like 'v1.0/groups*' }).Count | Should -Be 0
        @($script:Posts[0].Body.mobileAppAssignments)[0].target.groupId | Should -Be $groupId
    }

    It 'Throws when <Count> groups have the display name' -ForEach @(@{ Count = 0 }, @{ Count = 2 }) {
        $script:Groups = @(1..$Count | Where-Object { $Count -gt 0 } | ForEach-Object { [PSCustomObject]@{ id = "g$_"; displayName = 'Dup' } })
        { InModuleScope tcs.intune.packaging { Add-IntuneWin32AppAssignment -AppId 'app-1' -AssignmentType User-Group -AssignmentGroup 'Dup' } } | Should -Throw "*Found $Count groups*"
        $script:Posts.Count | Should -Be 0
    }

    It 'Adds an assignment filter by name' {
        InModuleScope tcs.intune.packaging { $null = Add-IntuneWin32AppAssignment -AppId 'app-1' -AssignmentType All-Devices -FilterRuleType Exclude -FilterRule 'Corporate devices' }
        $target = @($script:Posts[0].Body.mobileAppAssignments)[0].target
        $target.deviceAndAppManagementAssignmentFilterId | Should -Be 'filter-1'
        $target.deviceAndAppManagementAssignmentFilterType | Should -Be 'exclude'
    }

    It 'Keeps other assignments and replaces one to the same target' {
        $script:ExistingAssignments = @(
            [PSCustomObject]@{ id = 'a1'; intent = 'available'; target = [PSCustomObject]@{ '@odata.type' = '#microsoft.graph.groupAssignmentTarget'; groupId = 'group-1' }; settings = $null }
            [PSCustomObject]@{ id = 'a2'; intent = 'required'; target = [PSCustomObject]@{ '@odata.type' = '#microsoft.graph.groupAssignmentTarget'; groupId = 'other'; deviceAndAppManagementAssignmentFilterId = 'f9'; deviceAndAppManagementAssignmentFilterType = 'include' }; settings = $null }
        )
        InModuleScope tcs.intune.packaging { $null = Add-IntuneWin32AppAssignment -AppId 'app-1' -AssignmentType User-Group -AssignmentGroup 'Intune-AG-App-Required' }
        $assignments = @($script:Posts[0].Body.mobileAppAssignments)
        $assignments.Count | Should -Be 2
        $kept = $assignments | Where-Object { $_.target.groupId -eq 'other' }
        $kept.intent | Should -Be 'required'
        $kept.target.deviceAndAppManagementAssignmentFilterId | Should -Be 'f9'
        ($assignments | Where-Object { $_.target.groupId -eq 'group-1' }).intent | Should -Be 'required'
        $script:Gets | Should -Contain 'beta/deviceAppManagement/mobileApps/app-1/assignments'
    }

    It 'Rejects <Case>' -ForEach @(
        @{ Case = 'a group assignment without a group'; Arguments = @{ AssignmentType = 'User-Group' }; Message = '*needs AssignmentGroup*' }
        @{ Case = 'a group for all devices'; Arguments = @{ AssignmentType = 'All-Devices'; AssignmentGroup = 'x' }; Message = '*does not use AssignmentGroup*' }
        @{ Case = 'a filter without a filter type'; Arguments = @{ AssignmentType = 'All-Devices'; FilterRule = 'x' }; Message = '*used together*' }
    ) {
        { InModuleScope tcs.intune.packaging -Parameters @{ Arguments = $Arguments } { Add-IntuneWin32AppAssignment -AppId 'app-1' @Arguments } } | Should -Throw $Message
        $script:Posts.Count | Should -Be 0
    }
}

Describe 'Set-IntuneAppRelationship' {
    BeforeEach {
        $script:Posts = [System.Collections.Generic.List[object]]::new()
        Mock -ModuleName tcs.intune.packaging Invoke-MgGraphRequest {
            if ($Method -eq 'POST') {
                $script:Posts.Add([PSCustomObject]@{ Uri = $Uri; Body = ($Body | ConvertFrom-Json) })
                return
            }
            [PSCustomObject]@{
                value = @(
                    [PSCustomObject]@{ '@odata.type' = '#microsoft.graph.mobileAppDependency'; targetId = 'dep-1'; targetType = 'child'; dependencyType = 'detect' }
                    [PSCustomObject]@{ '@odata.type' = '#microsoft.graph.mobileAppSupersedence'; targetId = 'old-1'; targetType = 'child'; supersedenceType = 'update' }
                    [PSCustomObject]@{ '@odata.type' = '#microsoft.graph.mobileAppSupersedence'; targetId = 'newer'; targetType = 'parent'; supersedenceType = 'update' }
                )
            }
        }
    }

    It 'Keeps the existing child relationships, replaces one to the same app and drops parent relationships' {
        InModuleScope tcs.intune.packaging {
            Set-IntuneAppRelationship -AppId 'app-1' -Relationship ([ordered]@{ '@odata.type' = '#microsoft.graph.mobileAppSupersedence'; targetId = 'old-1'; supersedenceType = 'replace' })
        }
        $script:Posts[0].Uri | Should -Be 'beta/deviceAppManagement/mobileApps/app-1/updateRelationships'
        $relationships = @($script:Posts[0].Body.relationships)
        $relationships.Count | Should -Be 2
        ($relationships | Where-Object targetId -EQ 'dep-1').dependencyType | Should -Be 'detect'
        ($relationships | Where-Object targetId -EQ 'old-1').supersedenceType | Should -Be 'replace'
        $relationships.targetId | Should -Not -Contain 'newer'
    }
}