From b8770a26558857f0899388bb896d64201ca9faae Mon Sep 17 00:00:00 2001 From: Frederik Hjorslev Nylander Date: Wed, 11 Mar 2026 21:33:42 +0100 Subject: [PATCH] Fix duplicate results when using parameter UPN --- .../public/Device/Get-IntuneDeviceLogin.ps1 | 31 ++-- .../Device/Get-IntuneDeviceLogin.Tests.ps1 | 142 +++++++++++------- 2 files changed, 108 insertions(+), 65 deletions(-) diff --git a/src/functions/public/Device/Get-IntuneDeviceLogin.ps1 b/src/functions/public/Device/Get-IntuneDeviceLogin.ps1 index 2b85ae9..605c1a5 100644 --- a/src/functions/public/Device/Get-IntuneDeviceLogin.ps1 +++ b/src/functions/public/Device/Get-IntuneDeviceLogin.ps1 @@ -316,21 +316,18 @@ Write-Verbose -Message "Searching for devices where user '$targetUserId' has logged in" - # Get all managed devices with usersLoggedOn property - # Note: Graph API may not support filtering on usersLoggedOn collection, so we retrieve all and filter client-side - # Invoke-GraphGet automatically handles pagination + # Get all managed devices with usersLoggedOn property. + # Invoke-GraphGet already handles pagination, so we query once and filter client-side. $uri = "$baseUri`?`$select=id,deviceName,usersLoggedOn" - $allDevices = [System.Collections.Generic.List[object]]::new() - $nextUri = $uri - - while ($null -ne $nextUri) { - $resp = Invoke-GraphGet -Uri $nextUri + $resp = Invoke-GraphGet -Uri $uri + $allDevices = @() + if ($null -ne $resp) { if ($null -ne $resp.value) { - $allDevices.AddRange($resp.value) + $allDevices = @($resp.value) + } else { + $allDevices = @($resp) } - - $nextUri = $resp.'@odata.nextLink' } if ($allDevices.Count -eq 0) { @@ -341,10 +338,20 @@ Write-Verbose -Message "Checking $($allDevices.Count) managed devices for user logons" $matchCount = 0 + $seenResults = [System.Collections.Generic.HashSet[string]]::new() foreach ($device in $allDevices) { # Check if target user is in the usersLoggedOn collection $userLogon = $device.usersLoggedOn | Where-Object -FilterScript { $_.userId -eq $targetUserId } if ($userLogon) { + $latestUserLogon = $userLogon | + Sort-Object -Property lastLogOnDateTime -Descending | + Select-Object -First 1 + $logonTimestamp = [datetime]$latestUserLogon.lastLogOnDateTime + $resultKey = "{0}|{1}|{2}" -f $device.id, $targetUserId, $logonTimestamp.ToString('o') + if (-not $seenResults.Add($resultKey)) { + continue + } + $matchCount++ $user = Resolve-EntraUserById -UserId $targetUserId [PSCustomObject]@{ @@ -352,7 +359,7 @@ UserPrincipalName = $user.userPrincipalName DeviceId = $device.id UserId = $targetUserId - LastLogonDateTime = [datetime]$userLogon.lastLogOnDateTime + LastLogonDateTime = $logonTimestamp } } } diff --git a/tests/public/Device/Get-IntuneDeviceLogin.Tests.ps1 b/tests/public/Device/Get-IntuneDeviceLogin.Tests.ps1 index da4cc93..18736dd 100644 --- a/tests/public/Device/Get-IntuneDeviceLogin.Tests.ps1 +++ b/tests/public/Device/Get-IntuneDeviceLogin.Tests.ps1 @@ -812,16 +812,16 @@ Describe 'Get-IntuneDeviceLogin' { $results[0].DeviceId | Should -Be $testDeviceId2 } - It 'Should find device in second page during pagination' { + It 'Should find device when match appears later in aggregated paginated data' { # Arrange $mockUser = [PSCustomObject]@{ id = $testUserId userPrincipalName = $testUserPrincipalName } - # Simulate paginated response: first page has different user, second page has target user - $page1 = [PSCustomObject]@{ - value = @( + # Simulate aggregated output from Invoke-GraphGet after auto-pagination + $mockDevicesResponse = [PSCustomObject]@{ + value = @( [PSCustomObject]@{ id = 'c0000000-0000-0000-0000-000000000001' deviceName = 'OTHER-DEVICE-1' @@ -831,13 +831,7 @@ Describe 'Get-IntuneDeviceLogin' { lastLogOnDateTime = '2024-03-05T10:30:00Z' } ) - } - ) - '@odata.nextLink' = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$select=id,deviceName,usersLoggedOn&$skiptoken=xyz' - } - - $page2 = [PSCustomObject]@{ - value = @( + }, [PSCustomObject]@{ id = $testDeviceId1 deviceName = 'DEVICE-001' @@ -851,19 +845,12 @@ Describe 'Get-IntuneDeviceLogin' { ) } - $callCount = 0 Mock -CommandName 'Invoke-GraphGet' -MockWith { param([string]$Uri) - $callCount++ if ($Uri -match 'users/') { return $mockUser } else { - # Simulate pagination by returning page1 first, then page2 - if ($Uri -match 'skiptoken') { - return $page2 - } else { - return $page1 - } + return $mockDevicesResponse } } Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser } @@ -877,16 +864,16 @@ Describe 'Get-IntuneDeviceLogin' { $results[0].DeviceName | Should -Be 'DEVICE-001' } - It 'Should find multiple devices across multiple pages' { + It 'Should find multiple devices across paginated results' { # Arrange $mockUser = [PSCustomObject]@{ id = $testUserId userPrincipalName = $testUserPrincipalName } - # Simulate multiple pages with target user on both pages - $page1 = [PSCustomObject]@{ - value = @( + # Simulate aggregated output from Invoke-GraphGet after auto-pagination + $aggregatedResponse = [PSCustomObject]@{ + value = @( [PSCustomObject]@{ id = $testDeviceId1 deviceName = 'DEVICE-001' @@ -896,13 +883,7 @@ Describe 'Get-IntuneDeviceLogin' { lastLogOnDateTime = '2024-03-05T10:30:00Z' } ) - } - ) - '@odata.nextLink' = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$select=id,deviceName,usersLoggedOn&$skiptoken=abc' - } - - $page2 = [PSCustomObject]@{ - value = @( + }, [PSCustomObject]@{ id = $testDeviceId2 deviceName = 'DEVICE-002' @@ -921,11 +902,7 @@ Describe 'Get-IntuneDeviceLogin' { if ($Uri -match 'users/') { return $mockUser } else { - if ($Uri -match 'skiptoken') { - return $page2 - } else { - return $page1 - } + return $aggregatedResponse } } Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser } @@ -939,16 +916,16 @@ Describe 'Get-IntuneDeviceLogin' { $results[1].DeviceId | Should -Be $testDeviceId2 } - It 'Should handle pagination with no matches on first page but match on second' { + It 'Should handle paginated results where match appears later in the dataset' { # Arrange $mockUser = [PSCustomObject]@{ id = $testUserId userPrincipalName = $testUserPrincipalName } - # First page: no target user - $page1 = [PSCustomObject]@{ - value = @( + # Simulate aggregated output from Invoke-GraphGet after auto-pagination + $aggregatedResponse = [PSCustomObject]@{ + value = @( [PSCustomObject]@{ id = 'c0000000-0000-0000-0000-000000000001' deviceName = 'OTHER-DEVICE' @@ -963,14 +940,7 @@ Describe 'Get-IntuneDeviceLogin' { id = 'c0000000-0000-0000-0000-000000000002' deviceName = 'OTHER-DEVICE-2' usersLoggedOn = @() - } - ) - '@odata.nextLink' = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$select=id,deviceName,usersLoggedOn&$skiptoken=def' - } - - # Second page: match found - $page2 = [PSCustomObject]@{ - value = @( + }, [PSCustomObject]@{ id = $testDeviceId1 deviceName = 'DEVICE-ON-PAGE2' @@ -988,13 +958,9 @@ Describe 'Get-IntuneDeviceLogin' { param([string]$Uri) if ($Uri -match 'users/') { return $mockUser - } else { - if ($Uri -match 'skiptoken') { - return $page2 - } else { - return $page1 - } } + + return $aggregatedResponse } Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser } @@ -1006,6 +972,76 @@ Describe 'Get-IntuneDeviceLogin' { $results[0].DeviceId | Should -Be $testDeviceId1 $results[0].DeviceName | Should -Be 'DEVICE-ON-PAGE2' } + + It 'Should suppress duplicate rows when Graph returns duplicate device entries' { + # Arrange + $mockUser = [PSCustomObject]@{ + id = $testUserId + userPrincipalName = $testUserPrincipalName + } + + $duplicateResponse = [PSCustomObject]@{ + value = @( + [PSCustomObject]@{ + id = $testDeviceId1 + deviceName = 'DEVICE-001' + usersLoggedOn = @( + [PSCustomObject]@{ + userId = $testUserId + lastLogOnDateTime = '2024-03-05T10:30:00Z' + } + ) + }, + [PSCustomObject]@{ + id = $testDeviceId2 + deviceName = 'DEVICE-002' + usersLoggedOn = @( + [PSCustomObject]@{ + userId = $testUserId + lastLogOnDateTime = '2024-03-04T09:15:00Z' + } + ) + }, + [PSCustomObject]@{ + id = $testDeviceId1 + deviceName = 'DEVICE-001' + usersLoggedOn = @( + [PSCustomObject]@{ + userId = $testUserId + lastLogOnDateTime = '2024-03-05T10:30:00Z' + } + ) + }, + [PSCustomObject]@{ + id = $testDeviceId2 + deviceName = 'DEVICE-002' + usersLoggedOn = @( + [PSCustomObject]@{ + userId = $testUserId + lastLogOnDateTime = '2024-03-04T09:15:00Z' + } + ) + } + ) + } + + Mock -CommandName 'Invoke-GraphGet' -MockWith { + param([string]$Uri) + if ($Uri -match 'users/') { + return $mockUser + } + + return $duplicateResponse + } + Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser } + + # Act + $results = @(Get-IntuneDeviceLogin -UserPrincipalName $testUserPrincipalName) + + # Assert + $results.Count | Should -Be 2 + ($results | Select-Object -ExpandProperty DeviceId | Sort-Object) | Should -Be @($testDeviceId1, $testDeviceId2) + } } Context 'When called with ByUserId parameter set' {