From 7cac9ed3030bf4adb4ff8b5469502affe5e1d09f Mon Sep 17 00:00:00 2001 From: Joshua T Date: Wed, 9 Dec 2015 14:48:36 -0600 Subject: [PATCH] Added loop functionality to Get-JiraGroupMember. This function will now loop through and return all members of the given JIRA group by default, rather than stopping at 50 members. Fixes #14. --- .../Functions/Get-JiraGroupMember.Tests.ps1 | 246 +++++++++++------- PSJira/Functions/Get-JiraGroupMember.ps1 | 111 ++++++-- 2 files changed, 243 insertions(+), 114 deletions(-) diff --git a/PSJira/Functions/Get-JiraGroupMember.Tests.ps1 b/PSJira/Functions/Get-JiraGroupMember.Tests.ps1 index dd691ee4..d454dc7d 100644 --- a/PSJira/Functions/Get-JiraGroupMember.Tests.ps1 +++ b/PSJira/Functions/Get-JiraGroupMember.Tests.ps1 @@ -1,49 +1,127 @@ -$here = Split-Path -Parent $MyInvocation.MyCommand.Path -$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") -. "$here\$sut" - -InModuleScope PSJira { - - $showMockData = $false - - $jiraServer = 'http://jiraserver.example.com' - - $testUsername = 'powershell-test' - $testEmail = "$testUsername@example.com" - - $testGroupName = 'Test Group' - $testGroupNameEscaped = [System.Web.HttpUtility]::UrlPathEncode($testGroupName) - $testGroupSize = 1 - - # The REST result returned by the interan call within Get-JiraGroup - $restResultNoUsers = @" +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") +. "$here\$sut" + +InModuleScope PSJira { + + $ShowMockData = $false + $ShowDebugText = $false + + Describe "Get-JiraGroupMember" { + if ($ShowDebugText) + { + Mock "Write-Debug" { + Write-Host " [DEBUG] $Message" -ForegroundColor Yellow + } + } + + Mock Get-JiraConfigServer { + 'https://jira.example.com' + } + + # If we don't override this in a context or test, we don't want it to + # actually try to query a JIRA instance + Mock Invoke-JiraMethod {} + + Mock Invoke-JiraMethod -ModuleName PSJira -ParameterFilter { $Method -eq 'Get' -and $URI -like '*/rest/api/*/group?groupname=testgroup*' } { + ConvertFrom-Json @' { - "name": "$testGroupName", - "self": "$jiraServer/rest/api/2/group?groupname=$testGroupName", - "users": { - "size": $testGroupSize, - "items": [], - "max-results": 50, - "start-index": 0, - "end-index": 0 - }, - "expand": "users" + "Name": "testgroup", + "RestUrl": "https://jira.example.com/rest/api/2/group?groupname=testgroup", + "Size": 2 } -"@ - - $restResultWithUsers = @" +'@ + } + + Mock Get-JiraGroup -ModuleName PSJira { + $obj = [PSCustomObject] @{ + 'Name' = 'testgroup' + 'RestUrl' = 'https://jira.example.com/rest/api/2/group?groupname=testgroup' + 'Size' = 2 + } + $obj.PSObject.TypeNames.Insert(0, 'PSJira.Group') + Write-Output $obj + } + + Context "Sanity checking" { + $command = Get-Command -Name Get-JiraGroupMember + + function defParam($name) + { + It "Has a -$name parameter" { + $command.Parameters.Item($name) | Should Not BeNullOrEmpty + } + } + + defParam 'Group' + defParam 'StartIndex' + defParam 'MaxResults' + defParam 'Credential' + } + + Context "Behavior testing" { + Mock Invoke-JiraMethod -ModuleName PSJira { + if ($ShowMockData) + { + Write-Host " Mocked Invoke-JiraMethod" -ForegroundColor Cyan + Write-Host " [Uri] $Uri" -ForegroundColor Cyan + Write-Host " [Method] $Method" -ForegroundColor Cyan +# Write-Host " [Body] $Body" -ForegroundColor Cyan + } + } + + Mock Get-JiraUser -ModuleName PSJira { + [PSCustomObject] @{ + 'Name' = 'username' + } + } + + It "Obtains members about a provided group in JIRA" { + { Get-JiraGroupMember -Group testgroup } | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName PSJira -Exactly -Times 1 -Scope It -ParameterFilter { $Method -eq 'Get' -and $URI -like '*/rest/api/*/group?groupname=testgroup&expand=users*' } + } + + It "Supports the -StartIndex and -MaxResults parameters to page through search results" { + { Get-JiraGroupMember -Group testgroup -StartIndex 10 -MaxResults 50 } | Should Not Throw + # Expected: expand=users[10:60] (start index of 10, last index of 10+50) + # https://docs.atlassian.com/jira/REST/6.4.12/#d2e2307 + # Also, -like doesn't seem to "like" square brackets + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName PSJira -Exactly -Times 1 -Scope It -ParameterFilter { $Method -eq 'Get' -and $URI -like '*/rest/api/*/group?groupname=testgroup&expand=users*10:60*' } + } + + It "Returns all issues via looping if -MaxResults is not specified" { + + # In order to test this, we'll need a slightly more elaborate + # mock that actually returns some data. + + Mock Invoke-JiraMethod -ModuleName PSJira { + if ($ShowMockData) + { + Write-Host " Mocked Invoke-JiraMethod" -ForegroundColor Cyan + Write-Host " [Uri] $Uri" -ForegroundColor Cyan + Write-Host " [Method] $Method" -ForegroundColor Cyan + } + ConvertFrom-Json -InputObject @' { - "name": "$testGroupName", - "self": "$jiraServer/rest/api/2/group?groupname=$testGroupName", + "name": "testgroup", + "self": "https://jira.example.com/rest/api/2/group?groupname=testgroup", "users": { - "size": $testGroupSize, + "size": 2, "items": [ { - "self": "$jiraServer/rest/api/2/user?username=$testUsername", - "key": "$testUsername", - "name": "$testUsername", - "emailAddress": "$testEmail", - "displayName": "Powershell Test User", + "self": "https://jira.example.com/rest/api/2/user?username=testuser1", + "key": "testuser1", + "name": "testuser1", + "emailAddress": "testuser1@example.com", + "displayName": "Test User 1", + "active": true + }, + { + "self": "https://jira.example.com/rest/api/2/user?username=testuser2", + "key": "testuser2", + "name": "testuser2", + "emailAddress": "testuser2@example.com", + "displayName": "Test User 2", "active": true } ], @@ -52,60 +130,34 @@ InModuleScope PSJira { "end-index": 0 }, "expand": "users" -} -"@ - - Describe "Get-JiraGroupMember" { - - Mock Get-JiraConfigServer -ModuleName PSJira { - Write-Output $jiraServer - } - - Mock Get-JiraGroup -ModuleName PSJira { - ConvertTo-JiraGroup ( ConvertFrom-Json -InputObject $restResultNoUsers ) - } - - # This is called by Get-JiraGroupMember - user information included. - # Note that the URI is changed from "latest" to "2" since this is operating on the output from Get-JiraGroup, - # and JIRA never returns the "latest" symlink. - Mock Invoke-JiraMethod -ModuleName PSJira -ParameterFilter {$Method -eq 'Get' -and $URI -eq "$jiraServer/rest/api/2/group?groupname=$testGroupName&expand=users"} { - if ($ShowMockData) - { - Write-Host " Mocked Invoke-JiraMethod with GET method" -ForegroundColor Cyan - Write-Host " [Method] $Method" -ForegroundColor Cyan - Write-Host " [URI] $URI" -ForegroundColor Cyan - } - ConvertFrom-Json -InputObject $restResultWithUsers - } - - # Generic catch-all. This will throw an exception if we forgot to mock something. - Mock Invoke-JiraMethod -ModuleName PSJira { - Write-Host " Mocked Invoke-JiraMethod with no parameter filter." -ForegroundColor DarkRed - Write-Host " [Method] $Method" -ForegroundColor DarkRed - Write-Host " [URI] $URI" -ForegroundColor DarkRed - throw "Unidentified call to Invoke-JiraMethod" - } - -# Mock Write-Debug { -# Write-Host "DEBUG: $Message" -ForegroundColor Yellow -# } - - ############# - # Tests - ############# - - It "Returns the members of a given JIRA group" { - $members = Get-JiraGroupMember -Group $testGroupName - $members | Should Not BeNullOrEmpty - @($members).Count | Should Be $testGroupSize - } - - It "Returns results as PSJira.User objects" { - $members = Get-JiraGroupMember -Group $testGroupName - # Shenanigans to account for members being either an array or a single object - @(Get-Member -InputObject @($members)[0])[0].TypeName | Should Be 'PSJira.User' - } - } -} - - +} +'@ + } + + { Get-JiraGroupMember -Group testgroup } | Should Not Throw + + Assert-MockCalled -CommandName Get-JiraGroup -Exactly -Times 1 -Scope It -ParameterFilter { $GroupName -eq 'testgroup' } + Assert-MockCalled -CommandName Invoke-JiraMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Method -eq 'Get' -and $URI -like '*/rest/api/*/group?groupname=testgroup&expand=users*0:2*' } + + } + } + + Context "Input testing" { + It "Accepts a group name for the -Group parameter" { + { Get-JiraGroupMember -Group testgroup } | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName PSJira -Exactly -Times 1 -Scope It -ParameterFilter { $Method -eq 'Get' -and $URI -like '*/rest/api/*/group?groupname=testgroup&expand=users*' } + } + + It "Accepts a group object for the -InputObject parameter" { + $group = Get-JiraGroup -GroupName testgroup + + { Get-JiraGroupMember -Group $group } | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName PSJira -Exactly -Times 1 -Scope It -ParameterFilter { $Method -eq 'Get' -and $URI -like '*/rest/api/*/group?groupname=testgroup&expand=users*' } + + # We called Get-JiraGroup once manually, and it should be + # called twice by Get-JiraGroupMember. + Assert-MockCalled -CommandName Get-JiraGroup -Exactly -Times 3 -Scope It + } + } + } +} diff --git a/PSJira/Functions/Get-JiraGroupMember.ps1 b/PSJira/Functions/Get-JiraGroupMember.ps1 index b8df9986..a4fde837 100644 --- a/PSJira/Functions/Get-JiraGroupMember.ps1 +++ b/PSJira/Functions/Get-JiraGroupMember.ps1 @@ -16,6 +16,16 @@ function Get-JiraGroupMember [PSJira.Group] The group to query for members .OUTPUTS [PSJira.User[]] Members of the provided group + .NOTES + By default, this will return all active users who are members of the + given group. For large groups, this can take quite some time. + + To limit the number of group members returned, use + the MaxResults parameter. You can also combine this with the + StartIndex parameter to "page" through results. + + This function does not return inactive users. This appears to be a + limitation of JIRA's REST API. #> [CmdletBinding()] param( @@ -25,11 +35,42 @@ function Get-JiraGroupMember ValueFromPipelineByPropertyName = $true)] [Object] $Group, - # Credentials to use to connect to Jira. If not specified, this function will use anonymous access. + # Index of the first user to return. This can be used to "page" through + # users in a large group or a slow connection. + [Parameter(Mandatory = $false)] + [ValidateRange(0, [Int]::MaxValue)] + [Int] $StartIndex = 0, + + # Maximum number of results to return. By default, all users will be + # returned. + [Parameter(Mandatory = $false)] + [ValidateRange(0, [Int]::MaxValue)] + [Int] $MaxResults = 0, + + # Credentials to use to connect to JIRA. If not specified, this function will use anonymous access. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] $Credential ) + begin + { + # This is a parameter in Get-JiraIssue, but in testing, JIRA doesn't + # reliably return more than 50 results at a time. + $pageSize = 50 + + if ($MaxResults -eq 0) + { + Write-Debug "[Get-JiraGroupMember] MaxResults was not specified. Using loop mode to obtain all members." + $loopMode = $true + } else { + $loopMode = $false + if ($MaxResults -gt 50) + { + Write-Warning "JIRA's API may not properly support MaxResults values higher than 50 for this method. If you receive inconsistent results, do not pass the MaxResults parameter to this function to return all results." + } + } + } + process { Write-Debug "[Get-JiraGroupMember] Obtaining a reference to Jira group [$Group]" @@ -39,26 +80,62 @@ function Get-JiraGroupMember { foreach ($g in $groupObj) { - Write-Debug "[Get-JiraGroupMember] Asking JIRA for members of group [$g]" - $url = "$($g.RestUrl)&expand=users" - - Write-Debug "[Get-JiraGroupMember] Preparing for blastoff!" - $groupResult = Invoke-JiraMethod -Method Get -URI $url -Credential $Credential - - if ($groupResult) + if ($loopMode) { - # ConvertTo-JiraGroup contains logic to convert and add group members to - # group objects if the members are returned from JIRA. + # Using the Size property of the group object, iterate + # through all users in a given group. - Write-Debug "[Get-JiraGroupMember] Converting results to PSJira.Group and PSJira.User objects" - $groupObjResult = ConvertTo-JiraGroup -InputObject $groupResult + $totalResults = $g.Size + $allUsers = New-Object -TypeName System.Collections.ArrayList + Write-Debug "[Get-JiraGroupMember] Paging through all results (loop mode)" + + for ($i = 0; $i -lt $totalResults; $i = $i + $PageSize) + { + if ($PageSize -gt ($i + $totalResults)) + { + $thisPageSize = $totalResults - $i + } else { + $thisPageSize = $PageSize + } + $percentComplete = ($i / $totalResults) * 100 + Write-Progress -Activity 'Get-JiraGroupMember' -Status "Obtaining members ($i - $($i + $thisPageSize) of $totalResults)..." -PercentComplete $percentComplete + Write-Debug "[Get-JiraGroupMember] Obtaining members $i - $($i + $thisPageSize)..." + $thisSection = Get-JiraGroupMember -Group $g -StartIndex $i -MaxResults $thisPageSize -Credential $Credential + foreach ($t in $thisSection) + { + [void] $allUsers.Add($t) + } + } + + Write-Progress -Activity 'Get-JiraGroupMember' -Completed + Write-Output ($allUsers.ToArray()) - Write-Debug "[Get-JiraGroupMember] Outputting group members" - Write-Output $groupObjResult.Member } else { - # Something is wrong here...we didn't get back a result from JIRA when we *did* get a - # valid group from Get-JiraGroup earlier. - Write-Warning "Something strange happened when invoking JIRA method Get to URL [$url]" + # Since user is an expandable property of the returned + # group from JIRA, JIRA doesn't use the MaxResults argument + # found in other REST endpoints. Instead, we need to pass + # expand=users[0:15] for users 0-15 (inclusive). + $url = '{0}&expand=users[{1}:{2}]' -f $g.RestUrl, $StartIndex, ($StartIndex + $MaxResults) + + Write-Debug "[Get-JiraGroupMember] Preparing for blastoff!" + $groupResult = Invoke-JiraMethod -Method Get -URI $url -Credential $Credential + + if ($groupResult) + { + # ConvertTo-JiraGroup contains logic to convert and add + # users (group members) to user objects if the members + # are returned from JIRA. + + Write-Debug "[Get-JiraGroupMember] Converting results to PSJira.Group and PSJira.User objects" + $groupObjResult = ConvertTo-JiraGroup -InputObject $groupResult + + Write-Debug "[Get-JiraGroupMember] Outputting group members" + Write-Output $groupObjResult.Member + } else { + # Something is wrong here...we didn't get back a result from JIRA when we *did* get a + # valid group from Get-JiraGroup earlier. + Write-Warning "A JIRA group could not be found at URL [$url], even though this seems to be a valid group." + } } } } else {