diff --git a/CHANGELOG.md b/CHANGELOG.md index 067b839c..cd7f38c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/515) fr Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/508) from [Miguel Nieto](https://github.com/mnieto) the following: - Fix import-module loses previously stored configuration [493](https://github.com/MethodsAndPractices/vsteam/issues/493) +Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/523) from [Seva Alekseyev](https://github.com/sevaa) the following: +- Fix import-module loses previously stored configuration [442](https://github.com/MethodsAndPractices/vsteam/issues/442)[517](https://github.com/MethodsAndPractices/vsteam/issues/517) + ## 7.12.0 diff --git a/Source/Private/common.ps1 b/Source/Private/common.ps1 index 81f72427..b35162ca 100644 --- a/Source/Private/common.ps1 +++ b/Source/Private/common.ps1 @@ -1,8 +1,10 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path -[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "It is used in other files")] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'It is used in other files')] $profilesPath = "$HOME/vsteam_profiles.json" +Add-Type -AssemblyName System.Web + # This is the main function for calling TFS and VSTS. It handels the auth and format of the route. # If you need to call TFS or VSTS this is the function to use. function _callAPI { @@ -47,17 +49,24 @@ function _callAPI { process { # If the caller did not provide a Url build it. if (-not $Url) { - $buildUriParams = @{ } + $PSBoundParameters; + $buildUriParams = @{ } + $PSBoundParameters $extra = 'method', 'body', 'InFile', 'OutFile', 'ContentType', 'AdditionalHeaders' - foreach ($x in $extra) { $buildUriParams.Remove($x) | Out-Null } + foreach ($x in $extra) { + $buildUriParams.Remove($x) | Out-Null + } $Url = _buildRequestURI @buildUriParams } elseif ($QueryString) { # If the caller provided the URL and QueryString we need # to add the querystring now + $qs = [System.Web.HttpUtility]::ParseQueryString('') foreach ($key in $QueryString.keys) { - $Url += _appendQueryString -name $key -value $QueryString[$key] + if ($QueryString[$key]) { + $qs.Add($key, $QueryString[$key]) + } } + + $Url = _queryStringAppender -Url $Url -QueryString $qs } if ($body) { @@ -71,7 +80,7 @@ function _callAPI { $params.Add('TimeoutSec', (_getDefaultTimeout)) # always use utf8 and json as default content type instead of xml - if ($false -eq $PSBoundParameters.ContainsKey("ContentType")) { + if ($false -eq $PSBoundParameters.ContainsKey('ContentType')) { $params.Add('ContentType', 'application/json; charset=utf-8') } @@ -81,19 +90,19 @@ function _callAPI { # 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 - if (!$AdditionalHeaders.ContainsKey("Authorization")) { + if (!$AdditionalHeaders.ContainsKey('Authorization')) { if (_useWindowsAuthenticationOnPremise) { $params.Add('UseDefaultCredentials', $true) } elseif (_useBearerToken) { - $params['Headers'].Add("Authorization", "Bearer $env:TEAM_TOKEN") + $params['Headers'].Add('Authorization', "Bearer $env:TEAM_TOKEN") } else { - $params['Headers'].Add("Authorization", "Basic $env:TEAM_PAT") + $params['Headers'].Add('Authorization', "Basic $env:TEAM_PAT") } } - if ($AdditionalHeaders -and $AdditionalHeaders.PSObject.Properties.name -match "Keys") { + if ($AdditionalHeaders -and $AdditionalHeaders.PSObject.Properties.name -match 'Keys') { foreach ($key in $AdditionalHeaders.Keys) { $params['Headers'].Add($key, $AdditionalHeaders[$key]) } @@ -102,7 +111,9 @@ 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' - foreach ($e in $extra) { $params.Remove($e) | Out-Null } + foreach ($e in $extra) { + $params.Remove($e) | Out-Null + } try { $resp = Invoke-RestMethod @params @@ -122,6 +133,39 @@ function _callAPI { } } +<# +.SYNOPSIS + appends a query string to a url +.DESCRIPTION + appends a query string to a url +.PARAMETER Url + the url to append the query string to +.PARAMETER QueryString + the query string to append +.EXAMPLE + _queryStringAppender -Url 'https://dev.azure.com/contoso/MyFirstProject/_apis/build/builds' -QueryString @{ definition = 1; statusFilter = 'completed' } +#> +function _queryStringAppender { + param( + [Parameter(Mandatory = $true)] + [string]$Url, + [Parameter(Mandatory = $true)] + [Collections.Specialized.NameValueCollection]$QueryString + ) + + if ($QueryString.HasKeys()) { + # Do not assume that Url already contains a query string + # Crude check, but this will do + if ($Url.IndexOf('?') -gt 0) { + $Url += '&' + $QueryString.ToString() + } + else { + $Url += '?' + $QueryString.ToString() + } + } + + return $Url +} # General function to manage API Calls that involve a paged response, # either with a ContinuationToken property in the body payload or @@ -147,14 +191,15 @@ function _callAPIContinuationToken { [int]$MaxPages ) - if ($MaxPages -le 0){ + if ($MaxPages -le 0) { $MaxPages = [int32]::MaxValue } if ([string]::IsNullOrEmpty($ContinuationTokenName)) { if ($UseHeader.IsPresent) { - $ContinuationTokenName = "X-MS-ContinuationToken" - } else { - $ContinuationTokenName = "continuationToken" + $ContinuationTokenName = 'X-MS-ContinuationToken' + } + else { + $ContinuationTokenName = 'continuationToken' } } $i = 0 @@ -162,8 +207,9 @@ function _callAPIContinuationToken { $apiParameters = $url do { if ($UseHeader.IsPresent) { - throw "Continuation token from response headers not supported in this version" - } else { + throw 'Continuation token from response headers not supported in this version' + } + else { $resp = _callAPI -url $apiParameters $continuationToken = $resp."$ContinuationTokenName" $i++ @@ -228,17 +274,17 @@ function _testVariableGroupsSupport { function _supportsSecurityNamespace { _hasAccount - if (([vsteam_lib.Versions]::Version -ne "VSTS") -and ([vsteam_lib.Versions]::Version -ne "AzD")) { + if (([vsteam_lib.Versions]::Version -ne 'VSTS') -and ([vsteam_lib.Versions]::Version -ne 'AzD')) { throw 'Security Namespaces are currently only supported in Azure DevOps Service (Online)' } } function _supportsMemberEntitlementManagement { - [CmdletBinding(DefaultParameterSetName="upto")] + [CmdletBinding(DefaultParameterSetName = 'upto')] param( - [parameter(ParameterSetName="upto")] + [parameter(ParameterSetName = 'upto')] [string]$UpTo = $null, - [parameter(ParameterSetName="onwards")] + [parameter(ParameterSetName = 'onwards')] [string]$Onwards = $null ) @@ -246,9 +292,11 @@ function _supportsMemberEntitlementManagement { $apiVer = _getApiVersion MemberEntitlementManagement if (-not $apiVer) { throw 'This account does not support Member Entitlement.' - } elseif (-not [string]::IsNullOrEmpty($UpTo) -and $apiVer -gt $UpTo) { + } + 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) { + } + 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" } } @@ -270,7 +318,7 @@ function _getApiVersion { [parameter(ParameterSetName = 'Service', Mandatory = $true, Position = 0)] [ValidateSet('Build', 'Release', 'Core', 'Git', 'DistributedTask', 'DistributedTaskReleased', 'VariableGroups', 'Tfvc', - 'Packaging', 'MemberEntitlementManagement','Version', + 'Packaging', 'MemberEntitlementManagement', 'Version', 'ExtensionsManagement', 'ServiceEndpoints', 'Graph', 'TaskGroups', 'Policy', 'Processes', 'HierarchyQuery', 'Pipelines', 'Billing', 'Wiki', 'WorkItemTracking')] [string] $Service, @@ -280,7 +328,7 @@ function _getApiVersion { ) if ($Target.IsPresent) { - return [vsteam_lib.Versions]::GetApiVersion("Version") + return [vsteam_lib.Versions]::GetApiVersion('Version') } else { return [vsteam_lib.Versions]::GetApiVersion($Service) @@ -292,8 +340,8 @@ function _getInstance { } function _getDefaultTimeout { - if ($Global:PSDefaultParameterValues["*-vsteam*:vsteamApiTimeout"]) { - return $Global:PSDefaultParameterValues["*-vsteam*:vsteamApiTimeout"] + if ($Global:PSDefaultParameterValues['*-vsteam*:vsteamApiTimeout']) { + return $Global:PSDefaultParameterValues['*-vsteam*:vsteamApiTimeout'] } else { return 60 @@ -301,7 +349,7 @@ function _getDefaultTimeout { } function _getDefaultProject { - return $Global:PSDefaultParameterValues["*-vsteam*:projectName"] + return $Global:PSDefaultParameterValues['*-vsteam*:projectName'] } function _hasAccount { @@ -335,7 +383,9 @@ function _buildRequestURI { $sb = New-Object System.Text.StringBuilder - $instance = "https://dev.azure.com" + $qs = [System.Web.HttpUtility]::ParseQueryString('') + + $instance = 'https://dev.azure.com' if ($NoAccount.IsPresent -eq $false) { $instance = _getInstance } @@ -361,7 +411,7 @@ function _buildRequestURI { $sb.Append("/$team") | Out-Null } - $sb.Append("/_apis") | Out-Null + $sb.Append('/_apis') | Out-Null if ($area) { $sb.Append("/$area") | Out-Null @@ -376,17 +426,24 @@ function _buildRequestURI { } if ($version) { - $sb.Append("?api-version=$version") | Out-Null + $qs.Add('api-version', $version) } - $url = $sb.ToString() - if ($queryString) { foreach ($key in $queryString.keys) { - $Url += _appendQueryString -name $key -value $queryString[$key] + if ($QueryString[$key]) { + $qs.Add($key, $queryString[$key]) + } } } + if ($qs.HasKeys()) { + $sb.Append('?') | Out-Null + $sb.Append($qs.ToString()) | Out-Null + } + + $url = $sb.ToString() + return $url } } @@ -401,7 +458,7 @@ function _handleException { if ($ex.Exception.PSObject.Properties.Match('Response').count -gt 0 -and $null -ne $ex.Exception.Response -and - $ex.Exception.Response.StatusCode -ne "BadRequest") { + $ex.Exception.Response.StatusCode -ne 'BadRequest') { $handled = $true $msg = "An error occurred: $($ex.Exception.Message)" Write-Warning -Message $msg @@ -435,7 +492,7 @@ function _isVSTS { [parameter(Mandatory = $true)] [string] $instance ) - return $instance -like "*.visualstudio.com*" -or $instance -like "https://dev.azure.com/*" + return $instance -like '*.visualstudio.com*' -or $instance -like 'https://dev.azure.com/*' } function _getVSTeamAPIVersion { @@ -477,28 +534,6 @@ function _addSubDomain { return $instance } -function _appendQueryString { - param( - $name, - $value, - # When provided =0 will be outputed otherwise zeros will not be - # added. I had to add this for the userentitlements that is the only - # VSTS API I have found that requires Top and Skip to be passed in. - [Switch]$retainZero - ) - - if ($retainZero.IsPresent) { - if ($null -ne $value) { - return "&$name=$value" - } - } - else { - if ($value) { - return "&$name=$value" - } - } -} - function _getUserAgent { [CmdletBinding()] param() @@ -513,7 +548,7 @@ function _getUserAgent { } function _useWindowsAuthenticationOnPremise { - return (_isOnWindows) -and (!$env:TEAM_PAT) -and -not ($(_getInstance) -like "*visualstudio.com") -and -not ($(_getInstance) -like "https://dev.azure.com/*") + return (_isOnWindows) -and (!$env:TEAM_PAT) -and -not ($(_getInstance) -like '*visualstudio.com') -and -not ($(_getInstance) -like 'https://dev.azure.com/*') } function _useBearerToken { @@ -567,17 +602,17 @@ function _buildLevelDynamicParam { # Create and set the parameters' attributes $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $false - $ParameterAttribute.HelpMessage = "On Windows machines allows you to store the default project at the process, user or machine level. Not available on other platforms." + $ParameterAttribute.HelpMessage = 'On Windows machines allows you to store the default project at the process, user or machine level. Not available on other platforms.' # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) # Generate and set the ValidateSet if (_testAdministrator) { - $arrSet = "Process", "User", "Machine" + $arrSet = 'Process', 'User', 'Machine' } else { - $arrSet = "Process", "User" + $arrSet = 'Process', 'User' } $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) @@ -617,7 +652,7 @@ function _buildProjectNameDynamicParam { } $ParameterAttribute.ValueFromPipelineByPropertyName = $true - $ParameterAttribute.HelpMessage = "The name of the project. You can tab complete from the projects in your Team Services or TFS account when passed on the command line." + $ParameterAttribute.HelpMessage = 'The name of the project. You can tab complete from the projects in your Team Services or TFS account when passed on the command line.' # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) @@ -693,7 +728,7 @@ function _buildProcessNameDynamicParam { } $ParameterAttribute.ValueFromPipelineByPropertyName = $true - $ParameterAttribute.HelpMessage = "The name of the process. You can tab complete from the processes in your Team Services or TFS account when passed on the command line." + $ParameterAttribute.HelpMessage = 'The name of the process. You can tab complete from the processes in your Team Services or TFS account when passed on the command line.' # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) @@ -889,12 +924,12 @@ function _trackServiceEndpointProgress { $statusTracking = _callAPI -ProjectName $projectName -Area 'distributedtask' -Resource 'serviceendpoints' -Id $resp.id ` -Version $(_getApiVersion ServiceEndpoints) - $isReady = $statusTracking.isReady; + $isReady = $statusTracking.isReady if (-not $isReady) { $state = $statusTracking.operationStatus.state - if ($state -eq "Failed") { + if ($state -eq 'Failed') { throw $statusTracking.operationStatus.statusMessage } } @@ -920,9 +955,9 @@ function _getModuleVersion { } function _setEnvironmentVariables { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] param ( - [string] $Level = "Process", + [string] $Level = 'Process', [string] $Pat, [string] $Acct, [string] $BearerToken, @@ -939,31 +974,31 @@ function _setEnvironmentVariables { [vsteam_lib.Versions]::Account = $Acct # This is so it can be loaded by default in the next session - if ($Level -ne "Process") { - [System.Environment]::SetEnvironmentVariable("TEAM_PAT", $Pat, $Level) - [System.Environment]::SetEnvironmentVariable("TEAM_ACCT", $Acct, $Level) - [System.Environment]::SetEnvironmentVariable("TEAM_VERSION", $Version, $Level) + if ($Level -ne 'Process') { + [System.Environment]::SetEnvironmentVariable('TEAM_PAT', $Pat, $Level) + [System.Environment]::SetEnvironmentVariable('TEAM_ACCT', $Acct, $Level) + [System.Environment]::SetEnvironmentVariable('TEAM_VERSION', $Version, $Level) } } # If you remove an account the current default project needs to be cleared as well. function _clearEnvironmentVariables { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] param ( - [string] $Level = "Process" + [string] $Level = 'Process' ) $env:TEAM_PROJECT = $null $env:TEAM_TIMEOUT = $null [vsteam_lib.Versions]::DefaultProject = '' [vsteam_lib.Versions]::DefaultTimeout = '' - $Global:PSDefaultParameterValues.Remove("*-vsteam*:projectName") - $Global:PSDefaultParameterValues.Remove("*-vsteam*:vsteamApiTimeout") + $Global:PSDefaultParameterValues.Remove('*-vsteam*:projectName') + $Global:PSDefaultParameterValues.Remove('*-vsteam*:vsteamApiTimeout') # This is so it can be loaded by default in the next session - if ($Level -ne "Process") { - [System.Environment]::SetEnvironmentVariable("TEAM_PROJECT", $null, $Level) - [System.Environment]::SetEnvironmentVariable("TEAM_TIMEOUT", $null, $Level) + if ($Level -ne 'Process') { + [System.Environment]::SetEnvironmentVariable('TEAM_PROJECT', $null, $Level) + [System.Environment]::SetEnvironmentVariable('TEAM_TIMEOUT', $null, $Level) } _setEnvironmentVariables -Level $Level -Pat '' -Acct '' -UseBearerToken '' -Version '' @@ -978,7 +1013,7 @@ function _convertToHex() { $bytes = $Value | Format-Hex -Encoding Unicode $hexString = ($bytes.Bytes | ForEach-Object ToString X2) -join '' - return $hexString.ToLowerInvariant(); + return $hexString.ToLowerInvariant() } function _getVSTeamIdFromDescriptor { @@ -993,10 +1028,18 @@ function _getVSTeamIdFromDescriptor { # We need to Pad the string for FromBase64String to work reliably (AzD Descriptors are not padded) $ModulusValue = ($identifier.length % 4) Switch ($ModulusValue) { - '0' { $Padded = $identifier } - '1' { $Padded = $identifier.Substring(0, $identifier.Length - 1) } - '2' { $Padded = $identifier + ('=' * (4 - $ModulusValue)) } - '3' { $Padded = $identifier + ('=' * (4 - $ModulusValue)) } + '0' { + $Padded = $identifier + } + '1' { + $Padded = $identifier.Substring(0, $identifier.Length - 1) + } + '2' { + $Padded = $identifier + ('=' * (4 - $ModulusValue)) + } + '3' { + $Padded = $identifier + ('=' * (4 - $ModulusValue)) + } } return [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($Padded)) @@ -1020,47 +1063,47 @@ function _getPermissionInheritanceInfo { $projectId = (Get-VSTeamProject -Name $projectName | Select-Object -ExpandProperty id) Switch ($resourceType) { - "Repository" { - $securityNamespaceID = "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87" + 'Repository' { + $securityNamespaceID = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' - $repositoryId = (Get-VSTeamGitRepository -Name "$resourceName" -projectName $projectName | Select-Object -ExpandProperty id ) + $repositoryId = (Get-VSTeamGitRepository -Name "$resourceName" -ProjectName $projectName | Select-Object -ExpandProperty id ) if ($null -eq $repositoryId) { - Write-Error "Unable to retrieve repository information. Ensure that the resourceName provided matches a repository name exactly." + Write-Error 'Unable to retrieve repository information. Ensure that the resourceName provided matches a repository name exactly.' return } $token = "repoV2/$($projectId)/$repositoryId" } - "BuildDefinition" { - $securityNamespaceID = "33344d9c-fc72-4d6f-aba5-fa317101a7e9" + 'BuildDefinition' { + $securityNamespaceID = '33344d9c-fc72-4d6f-aba5-fa317101a7e9' - $buildDefinitionId = (Get-VSTeamBuildDefinition -projectName $projectName | Where-Object name -eq "$resourceName" | Select-Object -ExpandProperty id) + $buildDefinitionId = (Get-VSTeamBuildDefinition -ProjectName $projectName | Where-Object name -EQ "$resourceName" | Select-Object -ExpandProperty id) if ($null -eq $buildDefinitionId) { - Write-Error "Unable to retrieve build definition information. Ensure that the resourceName provided matches a build definition name exactly." + Write-Error 'Unable to retrieve build definition information. Ensure that the resourceName provided matches a build definition name exactly.' return } $token = "$($projectId)/$buildDefinitionId" } - "ReleaseDefinition" { - $securityNamespaceID = "c788c23e-1b46-4162-8f5e-d7585343b5de" + 'ReleaseDefinition' { + $securityNamespaceID = 'c788c23e-1b46-4162-8f5e-d7585343b5de' - $releaseDefinition = (Get-VSTeamReleaseDefinition -projectName $projectName | Where-Object -Property name -eq "$resourceName") + $releaseDefinition = (Get-VSTeamReleaseDefinition -ProjectName $projectName | Where-Object -Property name -EQ "$resourceName") if ($null -eq $releaseDefinition) { - Write-Error "Unable to retrieve release definition information. Ensure that the resourceName provided matches a release definition name exactly." + Write-Error 'Unable to retrieve release definition information. Ensure that the resourceName provided matches a release definition name exactly.' return } - if (($releaseDefinition).path -eq "/") { + if (($releaseDefinition).path -eq '/') { $token = "$($projectId)/$($releaseDefinition.id)" } else { - $token = "$($projectId)" + "$($releaseDefinition.path -replace "\\","/")" + "/$($releaseDefinition.id)" + $token = "$($projectId)" + "$($releaseDefinition.path -replace '\\','/')" + "/$($releaseDefinition.id)" } } } @@ -1075,16 +1118,16 @@ function _getPermissionInheritanceInfo { function _getDescriptorForACL { [cmdletbinding()] param( - [parameter(Mandatory = $true, ParameterSetName = "ByUser")] + [parameter(Mandatory = $true, ParameterSetName = 'ByUser')] [vsteam_lib.User]$User, - [parameter(MAndatory = $true, ParameterSetName = "ByGroup")] + [parameter(MAndatory = $true, ParameterSetName = 'ByGroup')] [vsteam_lib.Group]$Group ) if ($User) { switch ($User.Origin) { - "vsts" { + 'vsts' { $sid = _getVSTeamIdFromDescriptor -Descriptor $User.Descriptor if ($User.Descriptor.StartsWith('svc.')) { @@ -1094,20 +1137,24 @@ function _getDescriptorForACL { $descriptor = "Microsoft.TeamFoundation.Identity;$sid" } } - "aad" { + 'aad' { $descriptor = "Microsoft.IdentityModel.Claims.ClaimsIdentity;$($User.Domain)\\$($User.PrincipalName)" } - default { throw "User type not handled yet for ACL. Please report this as an issue on the VSTeam Repository: https://github.com/MethodsAndPractices/vsteam/issues" } + default { + throw 'User type not handled yet for ACL. Please report this as an issue on the VSTeam Repository: https://github.com/MethodsAndPractices/vsteam/issues' + } } } if ($Group) { switch ($Group.Origin) { - "vsts" { + 'vsts' { $sid = _getVSTeamIdFromDescriptor -Descriptor $Group.Descriptor $descriptor = "Microsoft.TeamFoundation.Identity;$sid" } - default { throw "Group type not handled yet for Add-VSTeamGitRepositoryPermission. Please report this as an issue on the VSTeam Repository: https://github.com/MethodsAndPractices/vsteam/issues" } + default { + throw 'Group type not handled yet for Add-VSTeamGitRepositoryPermission. Please report this as an issue on the VSTeam Repository: https://github.com/MethodsAndPractices/vsteam/issues' + } } } @@ -1138,9 +1185,9 @@ function _getBillingToken { $billingToken = _callAPI ` -NoProject ` -method POST ` - -ContentType "application/json" ` - -area "WebPlatformAuth" ` - -resource "SessionToken" ` + -ContentType 'application/json' ` + -area 'WebPlatformAuth' ` + -resource 'SessionToken' ` -version '3.2-preview.1' ` -body ($sessionToken | ConvertTo-Json -Depth 50 -Compress) @@ -1149,7 +1196,7 @@ function _getBillingToken { # pin if github is availabe and client has access to github function _pinpGithub { - Write-Verbose "Checking if client is online" + Write-Verbose 'Checking if client is online' $pingGh = [System.Net.NetworkInformation.Ping]::new() $replyStatus = $null try { @@ -1179,7 +1226,7 @@ function _showModuleLoadingMessages { # catch if web request fails. Invoke-WebRequest does not have a ErrorAction parameter try { - $moduleMessagesRes = (Invoke-RestMethod "https://raw.githubusercontent.com/MethodsAndPractices/vsteam/trunk/.github/moduleMessages.json") + $moduleMessagesRes = (Invoke-RestMethod 'https://raw.githubusercontent.com/MethodsAndPractices/vsteam/trunk/.github/moduleMessages.json') # don't show messages if module has not the specified version $filteredMessages = $moduleMessagesRes | Where-Object { @@ -1199,7 +1246,7 @@ function _showModuleLoadingMessages { # dont show messages if display until date is in the past $currentDate = Get-Date - $filteredMessages = $filteredMessages | Where-Object { $currentDate -le ([DateTime]::ParseExact($_.toDate, "dd/MM/yyyy HH:mm:ss", [cultureInfo]::InvariantCulture)) + $filteredMessages = $filteredMessages | Where-Object { $currentDate -le ([DateTime]::ParseExact($_.toDate, 'dd/MM/yyyy HH:mm:ss', [cultureInfo]::InvariantCulture)) } # stop processing if no messages left @@ -1208,7 +1255,7 @@ function _showModuleLoadingMessages { } $filteredMessages | ForEach-Object { - $messageFormat = "{0}: {1}" + $messageFormat = '{0}: {1}' Write-Information ($messageFormat -f $_.type.ToUpper(), $_.msg) -InformationAction Continue } } @@ -1217,7 +1264,7 @@ function _showModuleLoadingMessages { } } else { - Write-Information "Client is offline or blocked by a firewall. Skipping module messages" + Write-Information 'Client is offline or blocked by a firewall. Skipping module messages' } } } @@ -1240,10 +1287,10 @@ function _checkForModuleUpdates { # catch if web request fails. Invoke-WebRequest does not have a ErrorAction parameter try { - Write-Verbose "Checking if module is up to date" - $ghLatestRelease = Invoke-RestMethod "https://api.github.com/repos/MethodsAndPractices/vsteam/releases/latest" + Write-Verbose 'Checking if module is up to date' + $ghLatestRelease = Invoke-RestMethod 'https://api.github.com/repos/MethodsAndPractices/vsteam/releases/latest' - [version]$latestVersion = $ghLatestRelease.tag_name -replace "v", "" + [version]$latestVersion = $ghLatestRelease.tag_name -replace 'v', '' [version]$currentVersion = $ModuleVersion if ($currentVersion -lt $latestVersion) { @@ -1256,7 +1303,7 @@ function _checkForModuleUpdates { } } else { - Write-Information "Client is offline or blocked by a firewall. Skipping module updates check" + Write-Information 'Client is offline or blocked by a firewall. Skipping module updates check' } } @@ -1268,7 +1315,7 @@ function _countParameters() { ) $counter = 0 $advancedPameters = @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable') - foreach($p in $BoundParameters.GetEnumerator()) { + foreach ($p in $BoundParameters.GetEnumerator()) { if ($p.Key -notin $advancedPameters) { $counter++ } diff --git a/Source/Public/Get-VSTeamUserEntitlement.ps1 b/Source/Public/Get-VSTeamUserEntitlement.ps1 index 85383a6c..efe9e952 100644 --- a/Source/Public/Get-VSTeamUserEntitlement.ps1 +++ b/Source/Public/Get-VSTeamUserEntitlement.ps1 @@ -49,9 +49,11 @@ function Get-VSTeamUserEntitlement { $paramset = 'PagedParams', 'PagedFilter' if ($paramCounter -eq 0 -or $PSCmdlet.ParameterSetName -eq 'ByID') { _supportsMemberEntitlementManagement - } elseif ($paramset -contains $PSCmdlet.ParameterSetName) { + } + elseif ($paramset -contains $PSCmdlet.ParameterSetName) { _supportsMemberEntitlementManagement -Onwards '6.0' - } else { + } + else { _supportsMemberEntitlementManagement -UpTo '5.1' } @@ -79,6 +81,7 @@ function Get-VSTeamUserEntitlement { $listurl = _buildRequestURI @commonArgs $objs = @() Write-Verbose "Use continuation token: $useContinuationToken" + $qs = [System.Web.HttpUtility]::ParseQueryString('') if ($useContinuationToken) { if ($psCmdLet.ParameterSetName -eq 'PagedParams') { #parameter names must be lowercase, parameter values depends on the parameter @@ -94,22 +97,34 @@ function Get-VSTeamUserEntitlement { } $filter = $filter.SubString(0, $filter.Length - 5) } - $listurl += _appendQueryString -name "`$filter" -value $filter - $listurl += _appendQueryString -name "select" -value ($select -join ",") + if ($filter) { + $qs.Add("`$filter", $filter) + } + if ($select) { + $qs.Add('select', $select -join ',') + } + + + $listurl = _queryStringAppender -Url $listurl -QueryString $qs # Call the REST API Write-Verbose "API params: $listurl" - $items = _callAPIContinuationToken -Url $listurl -PropertyName "members" + $items = _callAPIContinuationToken -Url $listurl -PropertyName 'members' 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 ",") + } + else { + $qs.Add('top', $top) + $qs.Add('skip', $skip) + if ($select) { + $qs.Add('select', $select -join ',') + } - # Call the REST API + $listurl = _queryStringAppender -Url $listurl -QueryString $qs Write-Verbose "API params: $listurl" + + # Call the REST API $resp = _callAPI -url $listurl foreach ($item in $resp.members) { diff --git a/Tests/function/tests/Get-VSTeamApproval.Tests.ps1 b/Tests/function/tests/Get-VSTeamApproval.Tests.ps1 index 06db45cc..ac934d2c 100644 --- a/Tests/function/tests/Get-VSTeamApproval.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamApproval.Tests.ps1 @@ -49,7 +49,7 @@ Describe 'VSTeamApproval' -Tag 'unit', 'approvals' { -ParameterFilter { $Uri -like "*https://vsrm.dev.azure.com/test/project/_apis/release/approvals*" -and $Uri -like "*api-version=$(_getApiVersion Release)*" -and - $Uri -like "*assignedtoFilter=Test User*" -and + $Uri -like "*assignedtoFilter=Test+User*" -and $Uri -like "*includeMyGroupApprovals=true*" } } @@ -80,7 +80,7 @@ Describe 'VSTeamApproval' -Tag 'unit', 'approvals' { $Uri -like "*http://localhost:8080/tfs/defaultcollection/project/_apis/release/approvals*" -and $Uri -like "*api-version=$(_getApiVersion Release)*" -and $Uri -like "*statusFilter=Pending*" -and - $Uri -like "*assignedtoFilter=Test User*" -and + $Uri -like "*assignedtoFilter=Test+User*" -and $Uri -like "*includeMyGroupApprovals=true*" -and $Uri -like "*releaseIdsFilter=1*" } diff --git a/Tests/function/tests/Get-VSTeamArea.Tests.ps1 b/Tests/function/tests/Get-VSTeamArea.Tests.ps1 index b7e64a2c..cb5df924 100644 --- a/Tests/function/tests/Get-VSTeamArea.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamArea.Tests.ps1 @@ -31,7 +31,7 @@ Describe 'VSTeamArea' { Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Uri -like "https://dev.azure.com/test/Public Demo/_apis/wit/classificationnodes*" -and $Uri -like "*api-version=$(_getApiVersion Core)*" -and - $Uri -like "*Ids=1,2,3,4*" -and + $Uri -like "*Ids=1%2c2%2c3%2c4*" -and $Uri -like "*`$Depth=5*" } } diff --git a/Tests/function/tests/Get-VSTeamClassificationNode.Tests.ps1 b/Tests/function/tests/Get-VSTeamClassificationNode.Tests.ps1 index 45a0e871..d1ac3df7 100644 --- a/Tests/function/tests/Get-VSTeamClassificationNode.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamClassificationNode.Tests.ps1 @@ -15,7 +15,7 @@ Describe 'VSTeamClassificationNode' { BeforeAll { Mock Invoke-RestMethod { Open-SampleFile 'classificationNodeResult.json' } Mock Invoke-RestMethod { Open-SampleFile 'withoutChildNode.json' } -ParameterFilter { - $Uri -like "*Ids=43,44*" + $Uri -like "*Ids=43%2c44*" } } @@ -63,7 +63,7 @@ Describe 'VSTeamClassificationNode' { Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Uri -like "https://dev.azure.com/test/Public Demo/_apis/wit/classificationnodes*" -and $Uri -like "*api-version=$(_getApiVersion Core)*" -and - $Uri -like "*Ids=1,2,3,4*" + $Uri -like "*Ids=1%2c2%2c3%2c4*" } } @@ -75,7 +75,7 @@ Describe 'VSTeamClassificationNode' { Should -Invoke Invoke-RestMethod -Exactly 1 -ParameterFilter { $Uri -like "https://dev.azure.com/test/Public Demo/_apis/wit/classificationnodes*" -and $Uri -like "*api-version=$(_getApiVersion Core)*" -and - $Uri -like "*Ids=43,44*" + $Uri -like "*Ids=43%2c44*" } } } diff --git a/Tests/function/tests/Get-VSTeamGitCommit.Tests.ps1 b/Tests/function/tests/Get-VSTeamGitCommit.Tests.ps1 index 3ba8f5f7..e9c6d240 100644 --- a/Tests/function/tests/Get-VSTeamGitCommit.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamGitCommit.Tests.ps1 @@ -48,8 +48,8 @@ Describe "VSTeamGitCommit" { ## Assert Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { $Uri -like "*repositories/06E176BE-D3D2-41C2-AB34-5F4D79AEC86B/commits*" -and - $Uri -like "*searchCriteria.fromDate=2020-01-01T00:00:00Z*" -and - $Uri -like "*searchCriteria.toDate=2020-03-01T00:00:00Z*" -and + $Uri -like "*searchCriteria.fromDate=2020-01-01T00%3a00%3a00Z*" -and + $Uri -like "*searchCriteria.toDate=2020-03-01T00%3a00%3a00Z*" -and $Uri -like "*searchCriteria.itemVersion.versionType=commit*" -and $Uri -like "*searchCriteria.itemVersion.version=abcdef1234567890abcdef1234567890*" -and $Uri -like "*searchCriteria.itemVersion.versionOptions=previousChange*" -and diff --git a/Tests/function/tests/Get-VSTeamGitRef.Tests.ps1 b/Tests/function/tests/Get-VSTeamGitRef.Tests.ps1 index db7f9183..7030919c 100644 --- a/Tests/function/tests/Get-VSTeamGitRef.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamGitRef.Tests.ps1 @@ -72,7 +72,7 @@ Describe "VSTeamGitRef" { ## Assert Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { - $Uri -like "*filter=/refs/heads*" -and + $Uri -like "*filter=%2Frefs%2Fheads*" -and $Uri -like "*`$top=500*" -and $Uri -like "*filterContains=test*" } diff --git a/Tests/function/tests/Get-VSTeamGroup.Tests.ps1 b/Tests/function/tests/Get-VSTeamGroup.Tests.ps1 index ccb3e682..1c06790a 100644 --- a/Tests/function/tests/Get-VSTeamGroup.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamGroup.Tests.ps1 @@ -58,7 +58,7 @@ Describe "VSTeamGroup" { Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Uri -like "https://vssps.dev.azure.com/test/_apis/graph/groups*" -and $Uri -like "*api-version=$(_getApiVersion Graph)*" -and - $Uri -like "*subjectTypes=vssgp,aadgp*" + $Uri -like "*subjectTypes=vssgp%2caadgp*" } } @@ -68,7 +68,7 @@ Describe "VSTeamGroup" { Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Uri -like "https://vssps.dev.azure.com/test/_apis/graph/groups*" -and $Uri -like "*api-version=$(_getApiVersion Graph)*" -and - $Uri -like "*subjectTypes=vssgp,aadgp*" -and + $Uri -like "*subjectTypes=vssgp%2caadgp*" -and $Uri -like "*scopeDescriptor=scp.ZGU5ODYwOWEtZjRiMC00YWEzLTgzOTEtODI4ZDU2MDI0MjU2*" } } diff --git a/Tests/function/tests/Get-VSTeamIteration.Tests.ps1 b/Tests/function/tests/Get-VSTeamIteration.Tests.ps1 index 39a34d78..69aec965 100644 --- a/Tests/function/tests/Get-VSTeamIteration.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamIteration.Tests.ps1 @@ -17,7 +17,7 @@ Describe 'VSTeamIteration' { BeforeAll { Mock Invoke-RestMethod { Open-SampleFile 'classificationNodeResult.json' } Mock Invoke-RestMethod { Open-SampleFile 'withoutChildNode.json' } -ParameterFilter { - $Uri -like "*Ids=43,44*" + $Uri -like "*Ids=43%2c44*" } } @@ -41,7 +41,7 @@ Describe 'VSTeamIteration' { Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Uri -like "https://dev.azure.com/test/Public Demo/_apis/wit/classificationnodes*" -and $Uri -like "*api-version=$(_getApiVersion Core)*" -and - $Uri -like "*Ids=1,2,3,4*" -and + $Uri -like "*Ids=1%2c2%2c3%2c4*" -and $Uri -like "*`$Depth=5*" } } diff --git a/Tests/function/tests/Get-VSTeamPullRequest.Tests.ps1 b/Tests/function/tests/Get-VSTeamPullRequest.Tests.ps1 index 44eb5f4d..bc53ce52 100644 --- a/Tests/function/tests/Get-VSTeamPullRequest.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamPullRequest.Tests.ps1 @@ -112,7 +112,7 @@ Describe 'VSTeamPullRequest' { Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { $Uri -like "*api-version=$(_getApiVersion Git)*" -and $Uri -like "*Test/_apis/git*" -and - $Uri -like "*searchCriteria.sourceRefName=refs/heads/mybranch*" + $Uri -like "*searchCriteria.sourceRefName=refs%2fheads%2fmybranch*" } } @@ -122,7 +122,7 @@ Describe 'VSTeamPullRequest' { Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { $Uri -like "*api-version=$(_getApiVersion Git)*" -and $Uri -like "*Test/_apis/git*" -and - $Uri -like "*searchCriteria.targetRefName=refs/heads/mybranch*" + $Uri -like "*searchCriteria.targetRefName=refs%2fheads%2fmybranch*" } } diff --git a/Tests/function/tests/Get-VSTeamUser.Tests.ps1 b/Tests/function/tests/Get-VSTeamUser.Tests.ps1 index 56b84b3a..9de7f7c6 100644 --- a/Tests/function/tests/Get-VSTeamUser.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamUser.Tests.ps1 @@ -60,7 +60,7 @@ Describe 'VSTeamUser' { Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope Context -ParameterFilter { $Uri -like "https://vssps.dev.azure.com/test/_apis/graph/users*" -and $Uri -like "*api-version=$(_getApiVersion Graph)*" -and - $Uri -like "*subjectTypes=vss,aad*" + $Uri -like "*subjectTypes=vss%2caad*" } } } diff --git a/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 b/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 index 8aa66899..dba1ae8a 100644 --- a/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamUserEntitlement.Tests.ps1 @@ -86,10 +86,10 @@ Describe "VSTeamUserEntitlement" -Tag 'VSTeamUserEntitlement' { 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'$" + $Uri -like "*filter=userType+eq+%27guest%27" } Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamUserEntitlement.json' } -ParameterFilter { - $Uri -like "*filter=userType eq 'guest'&continuationToken=*" + $Uri -like "*filter=userType+eq+%27guest%27&continuationToken=*" } } @@ -118,7 +118,7 @@ Describe "VSTeamUserEntitlement" -Tag 'VSTeamUserEntitlement' { # 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'" + $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&`$filter=name+eq+%27Math%27" } } @@ -127,7 +127,7 @@ Describe "VSTeamUserEntitlement" -Tag 'VSTeamUserEntitlement' { # 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'" + $Uri -eq "https://vsaex.dev.azure.com/test/_apis/userentitlements?api-version=$(_getApiVersion MemberEntitlementManagement)&`$filter=name+eq+%27Math%27+and+licenseId+eq+%27Account-Advanced%27+and+userType+eq+%27member%27" } } diff --git a/Tests/function/tests/Get-VSTeamVariableGroup.Tests.ps1 b/Tests/function/tests/Get-VSTeamVariableGroup.Tests.ps1 index 6b7fb381..1650d269 100644 --- a/Tests/function/tests/Get-VSTeamVariableGroup.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamVariableGroup.Tests.ps1 @@ -70,6 +70,15 @@ Describe 'VSTeamVariableGroup' { $Uri -eq "http://localhost:8080/tfs/defaultcollection/project/_apis/distributedtask/variablegroups?api-version=$(_getApiVersion VariableGroups)&groupName=$varGroupName" } } + + It 'by name should support ampersend in name' { + $varGroupName = "Foo&Bar" + Get-VSTeamVariableGroup -projectName project -Name $varGroupName + + Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Uri -eq "http://localhost:8080/tfs/defaultcollection/project/_apis/distributedtask/variablegroups?api-version=$(_getApiVersion VariableGroups)&groupName=" + [System.Web.HttpUtility]::URLEncode($varGroupName) + } + } } } } \ No newline at end of file diff --git a/Tests/function/tests/Get-VSTeamWorkItem.Tests.ps1 b/Tests/function/tests/Get-VSTeamWorkItem.Tests.ps1 index ba21ecc8..7deb1888 100644 --- a/Tests/function/tests/Get-VSTeamWorkItem.Tests.ps1 +++ b/Tests/function/tests/Get-VSTeamWorkItem.Tests.ps1 @@ -27,7 +27,7 @@ Describe 'VSTeamWorkItem' { Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { $Uri -like "*https://dev.azure.com/test/_apis/wit/workitems*" -and $Uri -like "*api-version=$(_getApiVersion Core)*" -and - $Uri -like "*ids=47,48*" -and + $Uri -like "*ids=47%2c48*" -and $Uri -like "*`$Expand=None*" -and $Uri -like "*errorPolicy=omit*" }