From 47fbcd1639b9ce532e6fae04a0360d6ba352ec4c Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Wed, 4 Jan 2023 20:49:05 +0100 Subject: [PATCH 1/5] pick commit from trunk with _callAPIContinuationToken implementation --- Source/Private/common.ps1 | 88 ++++++++++++++++++++++++++- Tests/function/tests/common.Tests.ps1 | 27 ++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/Source/Private/common.ps1 b/Source/Private/common.ps1 index f24db4f5..0e6eada6 100644 --- a/Source/Private/common.ps1 +++ b/Source/Private/common.ps1 @@ -121,6 +121,64 @@ function _callAPI { } } + +# General function to manage API Calls that involve a paged response, +# either with a ContinuationToken property in the body payload or +# with a X-MS-ContinuationToken header +# TODO: Add functionality to manage paged responses based on X-MS-ContinuationToken header +# TODO: This would need to be integrated probably into the _callAPI function? +function _callAPIContinuationToken { + [CmdletBinding()] + param( + [string]$Url, + # If present, or $true, the function will manage the pages using the header + # specified in $ContinuationTokenName. + # If not present, or $false, the function will manage the pages using the + # continuationToken property specified in $ContinuationTokenName. + [switch]$UseHeader, + # Allows to specify a header or continuation token property different of the default values. + # If this parameter is not specified, the default value is X-MS-ContinuationToken or continuationToken + # depending if $UseHeader is present or not, respectively + [string]$ContinuationTokenName, + # Property in the response body payload that contains the collecion of objects to return to the calling function + [string]$PropertyName, + # Number of pages to be retrieved. If 0, or not specified, it will return all the available pages + [int]$MaxPages + ) + + if ($MaxPages -le 0){ + $MaxPages = [int32]::MaxValue + } + if ([string]::IsNullOrEmpty($ContinuationTokenName)) { + if ($UseHeader.IsPresent) { + $ContinuationTokenName = "X-MS-ContinuationToken" + } else { + $ContinuationTokenName = "continuationToken" + } + } + $i = 0 + $obj = @() + $apiParameters = $url + do { + if ($UseHeader.IsPresent) { + throw "Continuation token from response headers not supported in this version" + } else { + $resp = _callAPI -url $apiParameters + $continuationToken = $resp."$ContinuationTokenName" + $i++ + Write-Verbose "page $i" + $obj += $resp."$PropertyName" + if (-not [String]::IsNullOrEmpty($continuationToken)) { + $continuationToken = [uri]::EscapeDataString($continuationToken) + $apiParameters = "${url}&continuationToken=$continuationToken" + } + } + } while (-not [String]::IsNullOrEmpty($continuationToken) -and $i -lt $MaxPages) + + return $obj +} + + # Not all versions support the name features. function _supportsGraph { @@ -175,9 +233,22 @@ function _supportsSecurityNamespace { } function _supportsMemberEntitlementManagement { + [CmdletBinding(DefaultParameterSetName="upto")] + param( + [parameter(ParameterSetName="upto")] + [string]$UpTo = $null, + [parameter(ParameterSetName="onwards")] + [string]$Onwards = $null + + ) _hasAccount - if (-not $(_getApiVersion MemberEntitlementManagement)) { + $apiVer = _getApiVersion MemberEntitlementManagement + if (-not $apiVer) { throw 'This account does not support Member Entitlement.' + } elseif (-not [string]::IsNullOrEmpty($UpTo) -and $apiVer -gt $UpTo) { + throw "EntitlementManagemen version must be equal or lower than $UpTo for this call, current value $apiVer" + } elseif (-not [string]::IsNullOrEmpty($Onwards) -and $apiVer -lt $Onwards) { + throw "EntitlementManagemen version must be equal or greater than $Onwards for this call, current value $apiVer" } } @@ -1181,4 +1252,19 @@ function _checkForModuleUpdates { } } +} + +function _countParameters() { + param( + $BoundParameters + ) + $counter = 0 + $advancedPameters = @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable') + foreach($p in $BoundParameters.GetEnumerator()) { + if ($p.Key -notin $advancedPameters) { + $counter++ + } + } + Write-Verbose "Found $counter parameters" + $counter } \ No newline at end of file diff --git a/Tests/function/tests/common.Tests.ps1 b/Tests/function/tests/common.Tests.ps1 index 1a778f55..1ff8db6b 100644 --- a/Tests/function/tests/common.Tests.ps1 +++ b/Tests/function/tests/common.Tests.ps1 @@ -37,6 +37,33 @@ Describe 'Common' { } } + Context '_callAPIContinuationToken' { + BeforeAll { + Mock _callAPI { Open-SampleFile 'Get-VSTeamUserEntitlement-ContinuationToken.json' } -ParameterFilter { + $Url -match "filter=userType eq 'guest'$" + } + Mock _callAPI { Open-SampleFile 'Get-VSTeamUserEntitlement.json' } -ParameterFilter { + $Url -like "*filter=userType eq 'guest'&continuationToken=*" + } + } + + # TODO: To be removed when support to manage X-MS-ContinuationToken header is added and replace it with specific tests + It 'not supported should throw with UseHeader parameter' { + { _callAPIContinuationToken -UseHeader } | Should -Throw + } + + It 'When MaxPages has default value, all pages are returned' { + _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' + Should -Invoke _callAPI -Exactly -Times 2 + } + + It 'When number of pages are greater than MaxMages, only MaxPages are returned' { + _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' -MaxPages 1 + Should -Invoke _callAPI -Exactly -Times 1 + } + + } + Context '_getPermissionInheritanceInfo' { BeforeAll { Mock Get-VSTeamBuildDefinition { Open-SampleFile 'Get-BuildDefinition_AzD.json' -ReturnValue } From bdbb36a9708b6cc02055d316a699ebf599bbc8f0 Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Fri, 6 Jan 2023 00:44:33 +0100 Subject: [PATCH 2/5] integrate _callAPIContinuationToken into _callAPI --- Source/Private/common.ps1 | 88 ++++++++++++++++++++++----- Tests/function/tests/common.Tests.ps1 | 18 ++++++ 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/Source/Private/common.ps1 b/Source/Private/common.ps1 index 0e6eada6..3a61a04e 100644 --- a/Source/Private/common.ps1 +++ b/Source/Private/common.ps1 @@ -25,6 +25,9 @@ function _callAPI { [string]$Url, [object]$QueryString, [hashtable]$AdditionalHeaders = @{ }, + # Name of the output variable that will content the response headers + # Specify the name of the variable without the $ prefix + [string]$ResponseHeadersVariable, # Some API calls require the Project ID and not the project name. # However, the dynamic project name parameter only shows you names # and not the Project IDs. Using this flag the project name provided @@ -41,7 +44,22 @@ function _callAPI { # This flag makes sure that no specific account is used # some APIs do not have an account in their API uri because # they are not account specific in the url path itself. (e.g. user profile, pipeline billing) - [switch]$NoAccount + [switch]$NoAccount, + + + [ValidateSet('None', 'Header', 'Body')] + [string]$UseContinuationToken = 'None', + # Allows to specify a header or continuation token property different of the default values. + # If this parameter is not specified, the default value is X-MS-ContinuationToken or continuationToken + # depending if $UseHeader is present or not, respectively. Ignored if $UseContinuationToken -eq 'None' + [string]$ContinuationTokenName, + # Number of pages to be retrieved. If 0, or not specified, it will return all the available pages. + # Ignored if $UseContinuationToken -eq 'None' + [int]$MaxPages = 0, + # When using continuationToken, it is neccesary expand a specific property to get the real + # collection of objects. Ignored if $UseContinuationToken -eq 'None' + [string]$ColectionPropertyName + ) process { @@ -77,6 +95,24 @@ function _callAPI { # do not use header when requested. Then bearer must be provided with additional headers $params.Add('Headers', @{ }) + # # add response headers, if neccesary + # if (-not [string]::IsNullOrEmpty($ResponseHeadersVariable)) { + # $params.Add('ResponseHeadersVariable', 'ResponseHeaders') + # } + # configure continuationToken management + if ($UseContinuationToken -ne 'None') { + if ([strting]::IsNullOrEmpty($ContinuationTokenName)) { + if ($UseContinuationToken -eq 'Body') { + $ContinuationTokenName = 'continuationToken' + } else { + $ContinuationTokenName = 'X-MS-ContinuationToken' + } + } + } + if ($UseContinuationToken -eq 'Header') { + $params.Add('ResponseHeadersVariable', 'ResponseHeaders') + } + # checking if an authorization token is provided already with the additional headers # use case: sometimes other tokens for certain APIs have to be used (buying pipelines) in order to work # some parts of internal APIs use their own token based on the PAT @@ -99,25 +135,49 @@ function _callAPI { } # We have to remove any extra parameters not used by Invoke-RestMethod - $extra = 'NoAccount', 'NoProject', 'UseProjectId', 'Area', 'Resource', 'SubDomain', 'Id', 'Version', 'JSON', 'ProjectName', 'Team', 'Url', 'QueryString', 'AdditionalHeaders', 'CustomBearer' + $extra = 'NoAccount', 'NoProject', 'UseProjectId', 'Area', 'Resource', 'SubDomain', 'Id', 'Version', 'JSON', 'ProjectName', + 'Team', 'Url', 'QueryString', 'AdditionalHeaders', 'CustomBearer', 'UseContinuationToken', 'ContinuationTokenName', + 'MaxPages', 'ColectionPropertyName' foreach ($e in $extra) { $params.Remove($e) | Out-Null } - try { - $resp = Invoke-RestMethod @params + $page = 0 + $obj = @() + $requestUri = $params['Uri'] + do { + try { + $resp = Invoke-RestMethod @params - if ($resp) { - Write-Verbose "return type: $($resp.gettype())" - Write-Verbose $resp + if ($resp) { + Write-Verbose "return type: $($resp.gettype())" + Write-Verbose $resp + } + if ($UseContinuationToken -eq 'Body') { + $continuationToken = $resp."$ContinuationTokenName" + $continuationToken = [uri]::EscapeDataString($continuationToken) + $params['Uri'] = "${requestUri}&continuationToken=$continuationToken" + $obj += $resp."$ColectionPropertyName" + } elseif ($UseContinuationToken -eq 'Header') { + $continuationToken = $ResponseHeaders[$ContinuationTokenName] + $params['Uri'] = "${requestUri}&continuationToken=$continuationToken" + $obj += $resp."$ColectionPropertyName" + $obj += $resp + } else { + return $resp + } + $page++ + Write-Verbose "page $page" + # if (-not [string]::IsNullOrEmpty($ResponseHeadersVariable)) { + # New-Variable -Name $ResponseHeadersVariable -Value $ResponseHeaders -Scope 1 -Force # scope 1 creates the variable in the parent scope + # } } + catch { + _handleException $_ - return $resp - } - catch { - _handleException $_ - - throw - } + throw + } + } while (-not [String]::IsNullOrEmpty($continuationToken) -and $i -lt $MaxPages) + return $obj } } diff --git a/Tests/function/tests/common.Tests.ps1 b/Tests/function/tests/common.Tests.ps1 index 1ff8db6b..d7c3f12b 100644 --- a/Tests/function/tests/common.Tests.ps1 +++ b/Tests/function/tests/common.Tests.ps1 @@ -14,6 +14,13 @@ Describe 'Common' { Mock Invoke-RestMethod Mock _getApiVersion { return '1.0-unitTests' } Mock _getInstance { return 'https://dev.azure.com/test' } + + Mock _callAPI { Open-SampleFile 'Get-VSTeamUserEntitlement-ContinuationToken.json' } -ParameterFilter { + $Url -match "filter=userType eq 'guest'$" + } + Mock _callAPI { Open-SampleFile 'Get-VSTeamUserEntitlement.json' } -ParameterFilter { + $Url -like "*filter=userType eq 'guest'&continuationToken=*" + } } It 'Disable-VSTeamAgent' { @@ -35,6 +42,17 @@ Describe 'Common' { $Uri -eq "https://dev.azure.com/test/_apis/distributedtask/pools/36/agents/950?api-version=$(_getApiVersion DistributedTaskReleased)" } } + + It 'ContinuationToken in body, when MaxPages has default value, all pages are returned' { + _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' + Should -Invoke _callAPI -Exactly -Times 2 + } + + It 'ContinuationToken in body, when number of pages are greater than MaxMages, only MaxPages are returned' { + _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' -MaxPages 1 + Should -Invoke _callAPI -Exactly -Times 1 + } + } Context '_callAPIContinuationToken' { From 88af6dde5a9a7aee356c06fc0e0edd0fe962fd3d Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Fri, 6 Jan 2023 21:06:32 +0100 Subject: [PATCH 3/5] fixes in _callAPI --- Source/Private/common.ps1 | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/Source/Private/common.ps1 b/Source/Private/common.ps1 index 3a61a04e..90f6263b 100644 --- a/Source/Private/common.ps1 +++ b/Source/Private/common.ps1 @@ -45,8 +45,6 @@ function _callAPI { # some APIs do not have an account in their API uri because # they are not account specific in the url path itself. (e.g. user profile, pipeline billing) [switch]$NoAccount, - - [ValidateSet('None', 'Header', 'Body')] [string]$UseContinuationToken = 'None', # Allows to specify a header or continuation token property different of the default values. @@ -58,7 +56,7 @@ function _callAPI { [int]$MaxPages = 0, # When using continuationToken, it is neccesary expand a specific property to get the real # collection of objects. Ignored if $UseContinuationToken -eq 'None' - [string]$ColectionPropertyName + [string]$CollectionPropertyName ) @@ -95,18 +93,12 @@ function _callAPI { # do not use header when requested. Then bearer must be provided with additional headers $params.Add('Headers', @{ }) - # # add response headers, if neccesary - # if (-not [string]::IsNullOrEmpty($ResponseHeadersVariable)) { - # $params.Add('ResponseHeadersVariable', 'ResponseHeaders') - # } # configure continuationToken management - if ($UseContinuationToken -ne 'None') { - if ([strting]::IsNullOrEmpty($ContinuationTokenName)) { - if ($UseContinuationToken -eq 'Body') { - $ContinuationTokenName = 'continuationToken' - } else { - $ContinuationTokenName = 'X-MS-ContinuationToken' - } + if ($UseContinuationToken -ne 'None' -and [string]::IsNullOrEmpty($ContinuationTokenName)) { + if ($UseContinuationToken -eq 'Body') { + $ContinuationTokenName = 'continuationToken' + } else { + $ContinuationTokenName = 'X-MS-ContinuationToken' } } if ($UseContinuationToken -eq 'Header') { @@ -137,7 +129,7 @@ function _callAPI { # We have to remove any extra parameters not used by Invoke-RestMethod $extra = 'NoAccount', 'NoProject', 'UseProjectId', 'Area', 'Resource', 'SubDomain', 'Id', 'Version', 'JSON', 'ProjectName', 'Team', 'Url', 'QueryString', 'AdditionalHeaders', 'CustomBearer', 'UseContinuationToken', 'ContinuationTokenName', - 'MaxPages', 'ColectionPropertyName' + 'MaxPages', 'CollectionPropertyName' foreach ($e in $extra) { $params.Remove($e) | Out-Null } @@ -156,27 +148,23 @@ function _callAPI { $continuationToken = $resp."$ContinuationTokenName" $continuationToken = [uri]::EscapeDataString($continuationToken) $params['Uri'] = "${requestUri}&continuationToken=$continuationToken" - $obj += $resp."$ColectionPropertyName" + $obj += $resp."$CollectionPropertyName" } elseif ($UseContinuationToken -eq 'Header') { $continuationToken = $ResponseHeaders[$ContinuationTokenName] $params['Uri'] = "${requestUri}&continuationToken=$continuationToken" - $obj += $resp."$ColectionPropertyName" - $obj += $resp + $obj += $resp."$CollectionPropertyName" } else { return $resp } $page++ Write-Verbose "page $page" - # if (-not [string]::IsNullOrEmpty($ResponseHeadersVariable)) { - # New-Variable -Name $ResponseHeadersVariable -Value $ResponseHeaders -Scope 1 -Force # scope 1 creates the variable in the parent scope - # } } catch { _handleException $_ throw } - } while (-not [String]::IsNullOrEmpty($continuationToken) -and $i -lt $MaxPages) + } while (-not [string]::IsNullOrEmpty($continuationToken) -and $i -lt $MaxPages) return $obj } } From eb86ff23d905f749b1242426bebd39db05c2706d Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Fri, 6 Jan 2023 21:09:01 +0100 Subject: [PATCH 4/5] Get-VSTeamUserEntitlement supports continuationToken (picked from trunk) --- .docs/Get-VSTeamUserEntitlement.md | 125 +++++++- .docs/synopsis/Get-VSTeamUserEntitlement.md | 2 +- Source/Public/Get-VSTeamUserEntitlement.ps1 | 94 +++++- ...TeamUserEntitlement-ContinuationToken.json | 288 ++++++++++++++++++ .../tests/Get-VSTeamUserEntitlement.Tests.ps1 | 103 +++++-- 5 files changed, 579 insertions(+), 33 deletions(-) create mode 100644 Tests/SampleFiles/Get-VSTeamUserEntitlement-ContinuationToken.json diff --git a/.docs/Get-VSTeamUserEntitlement.md b/.docs/Get-VSTeamUserEntitlement.md index e148c0e4..ca9305a1 100644 --- a/.docs/Get-VSTeamUserEntitlement.md +++ b/.docs/Get-VSTeamUserEntitlement.md @@ -6,6 +6,11 @@ +Please note that Filter, Name, UserType and License parameters only works when MemberEntitlementManagement module version is 6.0 or upper +In the same way Top and Skip paramerers only works up to version 5.1 + +You can setup the specific version for the MemberEntitlementManagement calling Set-VSTeamAPIVersion -Service MemberEntitlementManagement -Version VersionNumberYouNeed. + ## SYNTAX ## DESCRIPTION @@ -14,6 +19,44 @@ ## EXAMPLES +### Example 1: Get user by Id + +```powershell +Get-VSTeamUserEntitlement -Id f1ef22eb-5dd6-4e26-907c-986a0311b106 +``` + +This command gets the user entitlement of the user identified by id. + +### Example 2: Get users by name + +```powershell +Get-VSTeamUserEntitlement -Name username +``` + +This command gets a list of users which mail or user name contains 'username'. +Filtering by Name, License, or UserType is available only when MemberEntitlementManagement service version is 6.0 or upper. See Get-VSTeamAPIVersion and Set-VSTeamAPIVersion commands + + +### Example 3: Filter with some conditions + +```powershell +Get-VSTeamUserEntitlement -Filter "licenseId eq 'Account-Express' and licenseStatus eq 'Disabled'" +``` + +This command gets a list of users that match the license status and license type conditions. +The -Filter parameter is available only when MemberEntitlementManagement service version is 6.0 or upper. See Get-VSTeamAPIVersion and Set-VSTeamAPIVersion commands + + +### Example 4: List paged users + +```powershell +Get-VSTeamUserEntitlement -Skip 100 -Top 100 +``` + +This command list the from the user in the 101 position, the next 100 users +Filtering using the -Top -Skip parameters only works when MemberEntitlementManagement service version is below 6.0. See Get-VSTeamAPIVersion and Set-VSTeamAPIVersion commands + + ## PARAMETERS ### Skip @@ -53,11 +96,91 @@ Comma (",") separated list of properties to select in the result entitlements. ```yaml Type: String -Parameter Sets: List +Parameter Sets: List,PagedFilter,PagedParams Required: True Default value: None ``` +### MaxPages + +User entlitement API returs a paged result. This parameter allows to limit the number of pages to be retrieved. Default is 0 = all pages. + +```yaml +Type: int +Parameter Sets: PagedFilter,PagedParams +Required: False +Default value: $null +``` + +### Filter + +Equality operators relating to searching user entitlements seperated by and clauses. Valid filters include: licenseId, licenseStatus, userType, and name. +- licenseId: filters based on license assignment using license names. i.e. licenseId eq 'Account-Stakeholder' or licenseId eq 'Account-Express'. +- licenseStatus: filters based on license status. currently only supports disabled. i.e. licenseStatus eq 'Disabled'. To get disabled basic licenses, you would pass (licenseId eq 'Account-Express' and licenseStatus eq 'Disabled') +- userType: filters off identity type. Suppored types are member or guest i.e. userType eq 'member'. +- name: filters on if the user's display name or email contians given input. i.e. get all users with "test" in email or displayname is "name eq 'test'". + +A valid query could be: (licenseId eq 'Account-Stakeholder' or (licenseId eq 'Account-Express' and licenseStatus eq 'Disabled')) and name eq 'test' and userType eq 'guest'. + +Currently, filter names and values must match exactly the case. i.e.: +* LicenseID will throw Invalid filter message. +* licenseId eq 'account-stakeholder' will return an empty list + +```yaml +Type: string +Parameter Sets: PagedFilter +Required: False +Default value: None +``` + +### License + +Filters based on license assignment using license names + +The acceptable values for this parameter are: +- Account-Stakeholder: Stakeholder +- Account-Express: Basic +- Account-Advanced: Basic + Test Plans + +Other licenses which source (licenseSource) is MSDN cannot be filtered here +Parameter values are case sensitive + +```yaml +Type: string +Parameter Sets: PagedParams +Required: False +Default value: None +``` + +### UserType + +Filters based on user type + +The acceptable values for this parameter are: +- member +- guest + +Parameter values are case sensitive + +```yaml +Type: string +Parameter Sets: PagedParams +Required: False +Default value: None +``` + +### Name + +Filters on if the user's display name or email contains given input + +```yaml +Type: string +Parameter Sets: PagedParams +Required: False +Default value: None +``` + + ## INPUTS ## OUTPUTS diff --git a/.docs/synopsis/Get-VSTeamUserEntitlement.md b/.docs/synopsis/Get-VSTeamUserEntitlement.md index 65b573ce..325fefd6 100644 --- a/.docs/synopsis/Get-VSTeamUserEntitlement.md +++ b/.docs/synopsis/Get-VSTeamUserEntitlement.md @@ -1 +1 @@ -Get User Entitlement for a user. \ No newline at end of file +Get User Entitlement for a user, or a paged list of users matching the specified filter \ No newline at end of file diff --git a/Source/Public/Get-VSTeamUserEntitlement.ps1 b/Source/Public/Get-VSTeamUserEntitlement.ps1 index 735a0f79..0143457c 100644 --- a/Source/Public/Get-VSTeamUserEntitlement.ps1 +++ b/Source/Public/Get-VSTeamUserEntitlement.ps1 @@ -9,22 +9,58 @@ function Get-VSTeamUserEntitlement { [int] $Skip = 0, [Parameter(ParameterSetName = 'List')] + [Parameter(ParameterSetName = 'PagedFilter')] + [Parameter(ParameterSetName = 'PagedParams')] [ValidateSet('Projects', 'Extensions', 'Grouprules')] [string[]] $Select, [Parameter(ParameterSetName = 'ByID')] [Alias('UserId')] - [string[]] $Id + [string[]] $Id, + + [Parameter(ParameterSetName = 'PagedFilter')] + [Parameter(ParameterSetName = 'PagedParams')] + [int] $MaxPages = 0, + + [Parameter(ParameterSetName = 'PagedFilter')] + [string] $Filter, + + [Parameter(ParameterSetName = 'PagedParams')] + [ValidateSet('Account-Stakeholder', 'Account-Express', 'Account-Advanced', IgnoreCase = $false)] + [Alias('License')] + [string] $LicenseId, + + [Parameter(ParameterSetName = 'PagedParams')] + [ValidateSet('guest', 'member', IgnoreCase = $false)] + [string] $UserType, + + [Parameter(ParameterSetName = 'PagedParams')] + [Alias('UserName')] + [Alias('Mail')] + [string] $Name ) + process { # This will throw if this account does not support MemberEntitlementManagement - _supportsMemberEntitlementManagement + # or supported version is not correct with the type of API call + $paramCounter = _countParameters -BoundParameters $PSBoundParameters + + $paramset = 'PagedParams', 'PagedFilter' + if ($paramCounter -eq 0 -or $PSCmdlet.ParameterSetName -eq 'ByID') { + _supportsMemberEntitlementManagement + } elseif ($paramset -contains $PSCmdlet.ParameterSetName) { + _supportsMemberEntitlementManagement -Onwards '6.0' + } else { + _supportsMemberEntitlementManagement -UpTo '5.1' + } + + $apiVersion = _getApiVersion MemberEntitlementManagement $commonArgs = @{ subDomain = 'vsaex' resource = 'userentitlements' - version = $(_getApiVersion MemberEntitlementManagement) + version = $apiVersion } if ($Id) { @@ -37,23 +73,53 @@ function Get-VSTeamUserEntitlement { } } else { - # Build the url to list the teams + # use the appropiate syntax depending on the API version + $useContinuationToken = ($paramCounter -eq 0 -and $apiVersion -gt '6.0') -or ($paramset -contains $PSCmdlet.ParameterSetName) + $listurl = _buildRequestURI @commonArgs + $objs = @() + Write-Verbose "Use continuation token: $useContinuationToken" + if ($useContinuationToken) { + if ($psCmdLet.ParameterSetName -eq 'PagedParams') { + #parameter names must be lowercase, parameter values depends on the parameter + if ($name) { + $filter += "name eq '$name' and " + } + if ($LicenseId) { - $listurl += _appendQueryString -name "top" -value $top -retainZero - $listurl += _appendQueryString -name "skip" -value $skip -retainZero - $listurl += _appendQueryString -name "select" -value ($select -join ",") + $filter += "licenseId eq '$LicenseId' and " + } + if ($UserType) { + $filter += "userType eq '$UserType' and " + } + $filter = $filter.SubString(0, $filter.Length - 5) + } + $listurl += _appendQueryString -name "`$filter" -value $filter + $listurl += _appendQueryString -name "select" -value ($select -join ",") - # Call the REST API - $resp = _callAPI -url $listurl + # Call the REST API + Write-Verbose "API params: $listurl" + # $items = _callAPIContinuationToken -Url $listurl -PropertyName "members" + $items = _callAPI -Url $listurl -CollectionPropertyName "members" -UseContinuationToken Body - $objs = @() + foreach ($item in $items) { + $objs += [vsteam_lib.UserEntitlement]::new($item) + } + } else { + $listurl += _appendQueryString -name "top" -value $top -retainZero + $listurl += _appendQueryString -name "skip" -value $skip -retainZero + $listurl += _appendQueryString -name "select" -value ($select -join ",") - foreach ($item in $resp.members) { - $objs += [vsteam_lib.UserEntitlement]::new($item) - } + # Call the REST API + Write-Verbose "API params: $listurl" + $resp = _callAPI -url $listurl + foreach ($item in $resp.members) { + $objs += [vsteam_lib.UserEntitlement]::new($item) + } + } Write-Output $objs + } } -} \ No newline at end of file +} diff --git a/Tests/SampleFiles/Get-VSTeamUserEntitlement-ContinuationToken.json b/Tests/SampleFiles/Get-VSTeamUserEntitlement-ContinuationToken.json new file mode 100644 index 00000000..256a4185 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamUserEntitlement-ContinuationToken.json @@ -0,0 +1,288 @@ +{ + "members": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "user": { + "subjectKind": "user", + "metaType": "member", + "domain": "00000000-0000-0000-0000-000000000000", + "principalName": "mlastName@test.com", + "mailAddress": "mlastName@test.com", + "origin": "aad", + "originId": "00000000-0000-0000-0000-000000000000", + "displayName": "Math lastName", + "_links": { + "self": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted" + }, + "memberships": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Memberships/aad.redacted" + }, + "membershipState": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/MembershipStates/aad.redacted" + }, + "storageKey": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/StorageKeys/aad.redacted" + }, + "avatar": { + "href": "https://dev.azure.com/toolTester/_apis/GraphProfile/MemberAvatars/aad.redacted" + } + }, + "url": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted", + "descriptor": "aad.redacted" + }, + "accessLevel": { + "licensingSource": "account", + "accountLicenseType": "earlyAdopter", + "msdnLicenseType": "none", + "licenseDisplayName": "Early Adopter", + "status": "active", + "statusMessage": "", + "assignmentSource": "unknown" + }, + "lastAccessedDate": "2020-09-09T06:43:29.0769722Z", + "dateCreated": "2020-02-25T05:27:12.1277176Z", + "projectEntitlements": [], + "extensions": [], + "groupAssignments": [] + }, + { + "id": "00000000-0000-0000-0000-000000000000", + "user": { + "subjectKind": "user", + "metaType": "guest", + "directoryAlias": "dlbm3_test.com#EXT#", + "domain": "00000000-0000-0000-0000-000000000000", + "principalName": "dlbm3@test.com", + "mailAddress": "dlbm3@test.com", + "origin": "aad", + "originId": "00000000-0000-0000-0000-000000000000", + "displayName": "Donovan Brown", + "_links": { + "self": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted" + }, + "memberships": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Memberships/aad.redacted" + }, + "membershipState": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/MembershipStates/aad.redacted" + }, + "storageKey": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/StorageKeys/aad.redacted" + }, + "avatar": { + "href": "https://dev.azure.com/toolTester/_apis/GraphProfile/MemberAvatars/aad.redacted" + } + }, + "url": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted", + "descriptor": "aad.redacted" + }, + "accessLevel": { + "licensingSource": "account", + "accountLicenseType": "earlyAdopter", + "msdnLicenseType": "none", + "licenseDisplayName": "Early Adopter", + "status": "pending", + "statusMessage": "", + "assignmentSource": "unknown" + }, + "lastAccessedDate": "0001-01-01T00:00:00Z", + "dateCreated": "2020-09-07T18:29:19.290717Z", + "projectEntitlements": [], + "extensions": [], + "groupAssignments": [] + }, + { + "id": "00000000-0000-0000-0000-000000000000", + "user": { + "subjectKind": "user", + "metaType": "member", + "directoryAlias": "test", + "domain": "00000000-0000-0000-0000-000000000000", + "principalName": "test@test.com", + "mailAddress": "test@test.com", + "origin": "aad", + "originId": "00000000-0000-0000-0000-000000000000", + "displayName": "Donovan Brown", + "_links": { + "self": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted" + }, + "memberships": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Memberships/aad.redacted" + }, + "membershipState": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/MembershipStates/aad.redacted" + }, + "storageKey": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/StorageKeys/aad.redacted" + }, + "avatar": { + "href": "https://dev.azure.com/toolTester/_apis/GraphProfile/MemberAvatars/aad.redacted" + } + }, + "url": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted", + "descriptor": "aad.redacted" + }, + "accessLevel": { + "licensingSource": "account", + "accountLicenseType": "earlyAdopter", + "msdnLicenseType": "none", + "licenseDisplayName": "Early Adopter", + "status": "active", + "statusMessage": "", + "assignmentSource": "unknown" + }, + "lastAccessedDate": "2020-09-09T15:38:08.1632123Z", + "dateCreated": "2017-12-24T16:41:16.743Z", + "projectEntitlements": [], + "extensions": [], + "groupAssignments": [] + } + ], + "continuationToken": "+RID:~I7MHAPrCvZ7TpEEAAAAAAA==#RT:1#TRC:100#ISV:2#IEO:65551#QCF:4#FPC:AggGAQAAAAAAAAIBAAAAJAAABgEAAAAAAAACANOkCQEAAAAAAAACAN6KFQEAAAAAAAACAKew/QAAAAAkAAACACCS/wAAAAAkAAACADmfAQEAAAAkAAACACK1AgEAAAAkAAAGAGO4kQAKAA==", + "totalCount": 0, + "items": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "user": { + "subjectKind": "user", + "metaType": "member", + "domain": "00000000-0000-0000-0000-000000000000", + "principalName": "mlastName@test.com", + "mailAddress": "mlastName@test.com", + "origin": "aad", + "originId": "00000000-0000-0000-0000-000000000000", + "displayName": "Math lastName", + "_links": { + "self": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted" + }, + "memberships": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Memberships/aad.redacted" + }, + "membershipState": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/MembershipStates/aad.redacted" + }, + "storageKey": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/StorageKeys/aad.redacted" + }, + "avatar": { + "href": "https://dev.azure.com/toolTester/_apis/GraphProfile/MemberAvatars/aad.redacted" + } + }, + "url": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted", + "descriptor": "aad.redacted" + }, + "accessLevel": { + "licensingSource": "account", + "accountLicenseType": "earlyAdopter", + "msdnLicenseType": "none", + "licenseDisplayName": "Early Adopter", + "status": "active", + "statusMessage": "", + "assignmentSource": "unknown" + }, + "lastAccessedDate": "2020-09-09T06:43:29.0769722Z", + "dateCreated": "2020-02-25T05:27:12.1277176Z", + "projectEntitlements": [], + "extensions": [], + "groupAssignments": [] + }, + { + "id": "00000000-0000-0000-0000-000000000000", + "user": { + "subjectKind": "user", + "metaType": "guest", + "directoryAlias": "dlbm3_test.com#EXT#", + "domain": "00000000-0000-0000-0000-000000000000", + "principalName": "dlbm3@test.com", + "mailAddress": "dlbm3@test.com", + "origin": "aad", + "originId": "00000000-0000-0000-0000-000000000000", + "displayName": "Donovan Brown", + "_links": { + "self": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted" + }, + "memberships": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Memberships/aad.redacted" + }, + "membershipState": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/MembershipStates/aad.redacted" + }, + "storageKey": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/StorageKeys/aad.redacted" + }, + "avatar": { + "href": "https://dev.azure.com/toolTester/_apis/GraphProfile/MemberAvatars/aad.redacted" + } + }, + "url": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted", + "descriptor": "aad.redacted" + }, + "accessLevel": { + "licensingSource": "account", + "accountLicenseType": "earlyAdopter", + "msdnLicenseType": "none", + "licenseDisplayName": "Early Adopter", + "status": "pending", + "statusMessage": "", + "assignmentSource": "unknown" + }, + "lastAccessedDate": "0001-01-01T00:00:00Z", + "dateCreated": "2020-09-07T18:29:19.290717Z", + "projectEntitlements": [], + "extensions": [], + "groupAssignments": [] + }, + { + "id": "00000000-0000-0000-0000-000000000000", + "user": { + "subjectKind": "user", + "metaType": "member", + "directoryAlias": "test", + "domain": "00000000-0000-0000-0000-000000000000", + "principalName": "test@test.com", + "mailAddress": "test@test.com", + "origin": "aad", + "originId": "00000000-0000-0000-0000-000000000000", + "displayName": "Donovan Brown", + "_links": { + "self": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted" + }, + "memberships": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Memberships/aad.redacted" + }, + "membershipState": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/MembershipStates/aad.redacted" + }, + "storageKey": { + "href": "https://vssps.dev.azure.com/toolTester/_apis/Graph/StorageKeys/aad.redacted" + }, + "avatar": { + "href": "https://dev.azure.com/toolTester/_apis/GraphProfile/MemberAvatars/aad.redacted" + } + }, + "url": "https://vssps.dev.azure.com/toolTester/_apis/Graph/Users/aad.redacted", + "descriptor": "aad.redacted" + }, + "accessLevel": { + "licensingSource": "account", + "accountLicenseType": "earlyAdopter", + "msdnLicenseType": "none", + "licenseDisplayName": "Early Adopter", + "status": "active", + "statusMessage": "", + "assignmentSource": "unknown" + }, + "lastAccessedDate": "2020-09-09T15:38:08.1632123Z", + "dateCreated": "2017-12-24T16:41:16.743Z", + "projectEntitlements": [], + "extensions": [], + "groupAssignments": [] + } + ] +} diff --git a/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 b/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 index 66bdae0a..8aa66899 100644 --- a/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 @@ -32,7 +32,7 @@ Describe "VSTeamUserEntitlement" -Tag 'VSTeamUserEntitlement' { BeforeAll { Mock _getApiVersion { return 'VSTS' } Mock _getInstance { return 'https://dev.azure.com/test' } - Mock _getApiVersion { return '1.0-unitTests' } -ParameterFilter { $Service -eq 'MemberEntitlementManagement' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamUserEntitlement.json' } Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamUserEntitlement-Id.json' } -ParameterFilter { @@ -40,18 +40,6 @@ Describe "VSTeamUserEntitlement" -Tag 'VSTeamUserEntitlement' { } } - It 'no parameters should return users' { - $users = Get-VSTeamUserEntitlement - - $users.count | Should -Be 3 - $users[0].UserName | Should -Be 'Math lastName' - - # Make sure it was called with the correct URI - Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { - $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&top=100&skip=0" - } - } - It 'by Id should return users with projects' { $user = Get-VSTeamUserEntitlement -Id '00000000-0000-0000-0000-000000000000' @@ -63,12 +51,93 @@ Describe "VSTeamUserEntitlement" -Tag 'VSTeamUserEntitlement' { } } - It 'with select for projects should return users with projects' { - Get-VSTeamUserEntitlement -Select Projects + Context 'Get-VSTeamUserEntitlement up to version 6.0' { + BeforeAll { + Mock _getApiVersion { return '1.0-unitTests' } -ParameterFilter { $Service -eq 'MemberEntitlementManagement' } + } + + It 'no parameters should return users' { + $users = Get-VSTeamUserEntitlement + + $users.count | Should -Be 3 + $users[0].UserName | Should -Be 'Math lastName' + + # Make sure it was called with the correct URI + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&top=100&skip=0" + } + } + + It 'with select for projects should return users with projects' { + Get-VSTeamUserEntitlement -Select Projects + + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&top=100&skip=0&select=Projects" + } + } - Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { - $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&top=100&skip=0&select=Projects" + It 'should throw with paged parameterset and version below 6.0 ' { + { Get-VSTeamUserEntitlement -Name 'Math' } | Should -Throw + } + + } + + Context 'Get-VSTeamUserEntitlement version 6.0 onwards' { + BeforeAll { + Mock _getApiVersion { return '6.0-unitTests' } -ParameterFilter { $Service -eq 'MemberEntitlementManagement' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamUserEntitlement-ContinuationToken.json' } -ParameterFilter { + $Uri -match "filter=userType eq 'guest'$" + } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamUserEntitlement.json' } -ParameterFilter { + $Uri -like "*filter=userType eq 'guest'&continuationToken=*" + } + } + + It "with incorrect case in license parameter should throw" { + { Get-VSTeamUserEntitlement -License account-Advanced} | Should -Throw + } + + It "with incorrect case in usertype parameter should throw" { + { Get-VSTeamUserEntitlement -UserType Member} | Should -Throw + } + + It 'no parameters should return users' { + $users = Get-VSTeamUserEntitlement + + $users.count | Should -Be 3 + $users[0].UserName | Should -Be 'Math lastName' + + # Make sure it was called with the correct URI + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)" + } + } + + It 'by filter should return users that match that filter' { + Get-VSTeamUserEntitlement -Filter "name eq 'Math'" + + # Make sure it was called with the correct URI parameters + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&`$filter=name eq 'Math'" + } + } + + It 'by name should translate to filter' { + Get-VSTeamUserEntitlement -Name "Math" -License Account-Advanced -UserType member + + # Make sure it was called with the correct URI parameters. Filter parameter names are case sensitive + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&`$filter=name eq 'Math' and licenseId eq 'Account-Advanced' and userType eq 'member'" + } + } + + It 'with many matches continuationToken is used' { + $users = Get-VSTeamUserEntitlement -UserType guest + $users.Count | Should -Be 6 + + Should -Invoke Invoke-RestMethod -Exactly -Times 2 } + } } } From 66c21a25729621cbd46f39ecf0c22dcec52b9f21 Mon Sep 17 00:00:00 2001 From: Miguel Nieto Date: Fri, 6 Jan 2023 21:33:56 +0100 Subject: [PATCH 5/5] remove _callAPIContinuationToken --- Source/Private/common.ps1 | 56 --------------------------- Tests/function/tests/common.Tests.ps1 | 33 ++-------------- 2 files changed, 3 insertions(+), 86 deletions(-) diff --git a/Source/Private/common.ps1 b/Source/Private/common.ps1 index 90f6263b..2af536f4 100644 --- a/Source/Private/common.ps1 +++ b/Source/Private/common.ps1 @@ -170,62 +170,6 @@ function _callAPI { } -# General function to manage API Calls that involve a paged response, -# either with a ContinuationToken property in the body payload or -# with a X-MS-ContinuationToken header -# TODO: Add functionality to manage paged responses based on X-MS-ContinuationToken header -# TODO: This would need to be integrated probably into the _callAPI function? -function _callAPIContinuationToken { - [CmdletBinding()] - param( - [string]$Url, - # If present, or $true, the function will manage the pages using the header - # specified in $ContinuationTokenName. - # If not present, or $false, the function will manage the pages using the - # continuationToken property specified in $ContinuationTokenName. - [switch]$UseHeader, - # Allows to specify a header or continuation token property different of the default values. - # If this parameter is not specified, the default value is X-MS-ContinuationToken or continuationToken - # depending if $UseHeader is present or not, respectively - [string]$ContinuationTokenName, - # Property in the response body payload that contains the collecion of objects to return to the calling function - [string]$PropertyName, - # Number of pages to be retrieved. If 0, or not specified, it will return all the available pages - [int]$MaxPages - ) - - if ($MaxPages -le 0){ - $MaxPages = [int32]::MaxValue - } - if ([string]::IsNullOrEmpty($ContinuationTokenName)) { - if ($UseHeader.IsPresent) { - $ContinuationTokenName = "X-MS-ContinuationToken" - } else { - $ContinuationTokenName = "continuationToken" - } - } - $i = 0 - $obj = @() - $apiParameters = $url - do { - if ($UseHeader.IsPresent) { - throw "Continuation token from response headers not supported in this version" - } else { - $resp = _callAPI -url $apiParameters - $continuationToken = $resp."$ContinuationTokenName" - $i++ - Write-Verbose "page $i" - $obj += $resp."$PropertyName" - if (-not [String]::IsNullOrEmpty($continuationToken)) { - $continuationToken = [uri]::EscapeDataString($continuationToken) - $apiParameters = "${url}&continuationToken=$continuationToken" - } - } - } while (-not [String]::IsNullOrEmpty($continuationToken) -and $i -lt $MaxPages) - - return $obj -} - # Not all versions support the name features. diff --git a/Tests/function/tests/common.Tests.ps1 b/Tests/function/tests/common.Tests.ps1 index d7c3f12b..08e9f138 100644 --- a/Tests/function/tests/common.Tests.ps1 +++ b/Tests/function/tests/common.Tests.ps1 @@ -44,39 +44,12 @@ Describe 'Common' { } It 'ContinuationToken in body, when MaxPages has default value, all pages are returned' { - _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' - Should -Invoke _callAPI -Exactly -Times 2 - } - - It 'ContinuationToken in body, when number of pages are greater than MaxMages, only MaxPages are returned' { - _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' -MaxPages 1 + _callAPI -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -CollectionPropertyName 'members' Should -Invoke _callAPI -Exactly -Times 1 } - } - - Context '_callAPIContinuationToken' { - BeforeAll { - Mock _callAPI { Open-SampleFile 'Get-VSTeamUserEntitlement-ContinuationToken.json' } -ParameterFilter { - $Url -match "filter=userType eq 'guest'$" - } - Mock _callAPI { Open-SampleFile 'Get-VSTeamUserEntitlement.json' } -ParameterFilter { - $Url -like "*filter=userType eq 'guest'&continuationToken=*" - } - } - - # TODO: To be removed when support to manage X-MS-ContinuationToken header is added and replace it with specific tests - It 'not supported should throw with UseHeader parameter' { - { _callAPIContinuationToken -UseHeader } | Should -Throw - } - - It 'When MaxPages has default value, all pages are returned' { - _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' - Should -Invoke _callAPI -Exactly -Times 2 - } - - It 'When number of pages are greater than MaxMages, only MaxPages are returned' { - _callAPIContinuationToken -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -PropertyName 'members' -MaxPages 1 + It 'ContinuationToken in body, when number of pages are greater than MaxMages, only MaxPages are returned' { + _callAPI -Url "https://vsaex.dev.azure.com/test/_apis/userentitlements?`$filter=userType eq 'guest'" -CollectionPropertyName 'members' -MaxPages 1 Should -Invoke _callAPI -Exactly -Times 1 }