diff --git a/CHANGELOG.md b/CHANGELOG.md index f53858c..0eeead6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,21 @@ All notable changes to this project will be documented in this file.
--> -## [0.4.0] + +## [UNRELEASED] + +### Summary +- _None_ + +### What's Changed +- feat(Get-AdoUser): Add user support (#124) + +### Breaking Changes +- _None_ + +
+ +## [0.4.0] - 2026-02-13 ### Summary Feature release adding repository initialization support, enhanced user entitlement data, and improved check/approval resource management. @@ -29,6 +43,7 @@ Feature release adding repository initialization support, enhanced user entitlem - feat: Add `ResourceId` parameter to all Check and Approval cmdlets - feat: Add `New-AdoPushInitialCommit` cmdlet for creating initial commits in Azure DevOps repositories - chore: Using namespace `System.Collections.Generic` declaration at the module root +- chore(Build.ps1): Update module version to 0.4.0 ### Breaking Changes - _None_ @@ -76,7 +91,6 @@ Hotfix release focused on bug fixes and code quality improvements. Achieved 100% - chore: Update CHANGELOG to reflect recent changes and fixes - chore: Update build version to 0.2.3 - ### Breaking Changes - With the output names update in `Add-AdoTeamIteration` and `Get-AdoTeamIteration` cmdlets the output names `team` and `project` are not available anymore, use `teamName` and `projectName` instead. diff --git a/docs/Get-AdoUser.md b/docs/Get-AdoUser.md new file mode 100644 index 0000000..1ee44e7 --- /dev/null +++ b/docs/Get-AdoUser.md @@ -0,0 +1,256 @@ + + + + + +# Get-AdoUser + +## SYNOPSIS + +Get a single or multiple users in an Azure DevOps organization. + +## SYNTAX + +### ListUsers (Default) + +```powershell +Get-AdoUser [-CollectionUri ] [-ScopeDescriptor ] [-SubjectTypes ] + [-Name ] [-Version ] [] +``` + +### ByDescriptor + +```powershell +Get-AdoUser [-CollectionUri ] [-UserDescriptor ] [-Version ] + [] +``` + +## ALIASES + +This cmdlet has the following aliases, +- N/A + +## DESCRIPTION + +This function retrieves a single or multiple users in an Azure DevOps organization through REST API. + +## EXAMPLES + +### EXAMPLE 1 + +#### PowerShell + +```powershell +Get-AdoUser +``` + +Retrieves all users in the Azure DevOps organization. + +### EXAMPLE 2 + +#### PowerShell + +```powershell +$project = Get-AdoProject -Name 'my-project-1' +$projectDescriptor = (Get-AdoDescriptor -StorageKey $project.Id) + +$params = @{ + CollectionUri = 'https://dev.azure.com/my-org' + ScopeDescriptor = $projectDescriptor + SubjectTypes = 'aad' +} +Get-AdoUser @params +``` + +Retrieves all users in the specified project with subject types 'aad'. + +### EXAMPLE 3 + +#### PowerShell + +```powershell +@( + 'aad.00000000-0000-0000-0000-000000000000', + 'aad.00000000-0000-0000-0000-000000000001', + 'aad.00000000-0000-0000-0000-000000000002' +) | Get-AdoUser +``` + +Retrieves the users with the specified descriptors. + +## PARAMETERS + +### -CollectionUri + +Optional. +The collection URI of the Azure DevOps collection/organization, e.g., . + +```yaml +Type: System.String +DefaultValue: $env:DefaultAdoCollectionUri +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Name + +Optional. +A user's display name to filter the retrieved results. + +```yaml +Type: System.String[] +DefaultValue: '' +SupportsWildcards: false +Aliases: +- DisplayName +- UserName +ParameterSets: +- Name: ListUsers + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -ScopeDescriptor + +Optional. +Specify a non-default scope (collection, project) to search for users. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ListUsers + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -SubjectTypes + +Optional. +A comma separated list of user subject subtypes to reduce the retrieved results, e.g. +'msa', 'aad', 'svc' (service identity), 'imp' (imported identity), etc. + +```yaml +Type: System.String[] +DefaultValue: "@('msa', 'aad', 'svc', 'imp')" +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ListUsers + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -UserDescriptor + +Optional. +The descriptor of a specific user to retrieve. +When provided, retrieves a single user by its descriptor. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: ByDescriptor + Position: Named + IsRequired: false + ValueFromPipeline: true + ValueFromPipelineByPropertyName: true + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Version + +Optional. +The API version to use for the request. +Default is '7.2-preview.1'. +The -preview flag must be supplied in the api-version for this request to work. + +```yaml +Type: System.String +DefaultValue: 7.2-preview.1 +SupportsWildcards: false +Aliases: +- ApiVersion +ParameterSets: +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: +- 7.1-preview.1 +- 7.2-preview.1 +HelpMessage: The -preview flag must be supplied in the api-version for this request to work. +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +- N/A + +## OUTPUTS + +### PSCustomObject + +## NOTES + +Retrieves users in an Azure DevOps organization. + + +## RELATED LINKS + +- +- diff --git a/src/Azure.DevOps.PSModule/Azure.DevOps.PSModule.psd1 b/src/Azure.DevOps.PSModule/Azure.DevOps.PSModule.psd1 index 97f7e36..0572ec0 100644 --- a/src/Azure.DevOps.PSModule/Azure.DevOps.PSModule.psd1 +++ b/src/Azure.DevOps.PSModule/Azure.DevOps.PSModule.psd1 @@ -94,6 +94,7 @@ 'Get-AdoTeamIteration' 'Get-AdoTeamIterationList' 'Get-AdoTeamSettings' + 'Get-AdoUser' 'Get-AdoUserEntitlement' 'New-AdoCheckApproval' 'New-AdoCheckBranchControl' diff --git a/src/Azure.DevOps.PSModule/Public/Graph/Users/Get-AdoUser.ps1 b/src/Azure.DevOps.PSModule/Public/Graph/Users/Get-AdoUser.ps1 new file mode 100644 index 0000000..13dcdd4 --- /dev/null +++ b/src/Azure.DevOps.PSModule/Public/Graph/Users/Get-AdoUser.ps1 @@ -0,0 +1,197 @@ +function Get-AdoUser { + <# + .SYNOPSIS + Get a single or multiple users in an Azure DevOps organization. + + .DESCRIPTION + This function retrieves a single or multiple users in an Azure DevOps organization through REST API. + + .PARAMETER CollectionUri + Optional. The collection URI of the Azure DevOps collection/organization, e.g., https://vssps.dev.azure.com/my-org. + + .PARAMETER ScopeDescriptor + Optional. Specify a non-default scope (collection, project) to search for users. + + .PARAMETER SubjectTypes + Optional. A comma separated list of user subject subtypes to reduce the retrieved results, e.g. 'msa', 'aad', 'svc' (service identity), 'imp' (imported identity), etc. + + .PARAMETER Name + Optional. A user's display name to filter the retrieved results. + + .PARAMETER UserDescriptor + Optional. The descriptor of a specific user to retrieve. When provided, retrieves a single user by its descriptor. + + .PARAMETER Version + The API version to use for the request. Default is '7.2-preview.1'. + The -preview flag must be supplied in the api-version for this request to work. + + .OUTPUTS + PSCustomObject + + .LINK + - https://learn.microsoft.com/en-us/rest/api/azure/devops/graph/users/get + - https://learn.microsoft.com/en-us/rest/api/azure/devops/graph/users/list + + .EXAMPLE + Get-AdoUser + + Retrieves all users in the Azure DevOps organization. + + .EXAMPLE + $project = Get-AdoProject -Name 'my-project-1' + $projectDescriptor = (Get-AdoDescriptor -StorageKey $project.Id) + + $params = @{ + CollectionUri = 'https://dev.azure.com/my-org' + ScopeDescriptor = $projectDescriptor + SubjectTypes = 'aad' + } + Get-AdoUser @params + + Retrieves all users in the specified project with subject types 'aad'. + + .EXAMPLE + @( + 'aad.00000000-0000-0000-0000-000000000000', + 'aad.00000000-0000-0000-0000-000000000001', + 'aad.00000000-0000-0000-0000-000000000002' + ) | Get-AdoUser + + Retrieves the users with the specified descriptors. + + .NOTES + Retrieves users in an Azure DevOps organization. + #> + [CmdletBinding(DefaultParameterSetName = 'ListUsers')] + [OutputType([PSCustomObject])] + param ( + [Parameter(ValueFromPipelineByPropertyName)] + [string]$CollectionUri = $env:DefaultAdoCollectionUri, + + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ListUsers')] + [string]$ScopeDescriptor, + + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ListUsers')] + [ValidateSet('msa', 'aad', 'svc', 'imp')] + [string[]]$SubjectTypes = @('msa', 'aad', 'svc', 'imp'), + + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ListUsers')] + [Alias('DisplayName', 'UserName')] + [string[]]$Name, + + [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, ParameterSetName = 'ByDescriptor')] + [string]$UserDescriptor, + + [Parameter(HelpMessage = 'The -preview flag must be supplied in the api-version for this request to work.')] + [Alias('ApiVersion')] + [ValidateSet('7.1-preview.1', '7.2-preview.1')] + [string]$Version = '7.2-preview.1' + ) + + begin { + Write-Verbose ("Command: $($MyInvocation.MyCommand.Name)") + Write-Debug ("CollectionUri: $CollectionUri") + Write-Debug ("ScopeDescriptor: $ScopeDescriptor") + Write-Debug ("SubjectTypes: $($SubjectTypes -join ',')") + Write-Debug ("Name: $($Name -join ',')") + Write-Debug ("UserDescriptor: $UserDescriptor") + Write-Debug ("Version: $Version") + + Confirm-Default -Defaults ([ordered]@{ + 'CollectionUri' = $CollectionUri + }) + + if ($CollectionUri -notmatch 'vssps\.') { + $CollectionUri = $CollectionUri -replace 'https://', 'https://vssps.' + } + } + + process { + try { + $queryParameters = [List[string]]::new() + + if ($UserDescriptor) { + $uri = "$CollectionUri/_apis/graph/users/$UserDescriptor" + } else { + $uri = "$CollectionUri/_apis/graph/users" + + if ($ScopeDescriptor) { + $queryParameters.Add("scopeDescriptor=$($ScopeDescriptor)") + } + + if ($SubjectTypes) { + $queryParameters.Add("subjectTypes=$([string]::Join(',', $SubjectTypes))") + } + } + + $params = @{ + Uri = $uri + Version = $Version + QueryParameters = if ($queryParameters.Count -gt 0) { $queryParameters -join '&' } else { $null } + Method = 'GET' + } + + try { + $continuationToken = $null + + do { + $pagedParams = [List[string]]::new() + + if ($queryParameters.Count) { + $pagedParams.AddRange($queryParameters) + } + if ($continuationToken) { + $pagedParams.Add("continuationToken=$([uri]::EscapeDataString($continuationToken))") + } + + $params.QueryParameters = if ($pagedParams.Count) { $pagedParams -join '&' } else { $null } + + $results = Invoke-AdoRestMethod @params + $users = if ($UserDescriptor) { @($results) } else { $results.value } + + if ($Name) { + $users = foreach ($n_ in $Name) { + $users | Where-Object { -not $n_ -or $_.displayName -like $n_ } + } + } + + foreach ($u_ in $users) { + $obj = [ordered]@{ + subjectKind = $u_.subjectKind + directoryAlias = $u_.directoryAlias + domain = $u_.domain + principalName = $u_.principalName + mailAddress = $u_.mailAddress + origin = $u_.origin + originId = $u_.originId + displayName = $u_.displayName + descriptor = $u_.descriptor + isDeletedInOrigin = $u_.isDeletedInOrigin + metaType = $u_.metaType + collectionUri = $CollectionUri + } + [PSCustomObject]$obj + } + + $continuationToken = ($results.continuationToken | Select-Object -First 1) + + } while ($continuationToken) + } catch { + if ($_.ErrorDetails.Message -match 'InvalidSubjectTypeException') { + Write-Warning "Subject with scope descriptor $ScopeDescriptor does not exist, skipping." + } elseif ($_.ErrorDetails.Message -match 'GraphSubjectNotFoundException') { + Write-Warning "Subject with user descriptor $UserDescriptor does not exist, skipping." + } else { + throw $_ + } + } + + } catch { + throw $_ + } + } + + end { + Write-Verbose ("Exit: $($MyInvocation.MyCommand.Name)") + } +} diff --git a/src/Azure.DevOps.PSModule/Tests/Graph/Users/Get-AdoUser.Tests.ps1 b/src/Azure.DevOps.PSModule/Tests/Graph/Users/Get-AdoUser.Tests.ps1 new file mode 100644 index 0000000..1b5a24d --- /dev/null +++ b/src/Azure.DevOps.PSModule/Tests/Graph/Users/Get-AdoUser.Tests.ps1 @@ -0,0 +1,351 @@ +BeforeAll { + # Import the module + $modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\..' + $moduleName = Join-Path -Path $modulePath -ChildPath 'Azure.DevOps.PSModule\Azure.DevOps.PSModule.psd1' + + # Remove module if already loaded + Get-Module Azure.DevOps.PSModule | Remove-Module -Force + + # Import the module + Import-Module $moduleName -Force -Verbose:$false +} + +Describe 'Get-AdoUser' { + BeforeAll { + # Sample response data for mocking + $mockCollectionUri = 'https://vssps.dev.azure.com/my-org' + $mockUserDescriptor = 'aad.00000000-0000-0000-0000-000000000001' + $mockScopeDescriptor = 'scp.00000000-0000-0000-0000-000000000002' + + $mockUser1 = [PSCustomObject]@{ + subjectKind = 'user' + directoryAlias = 'testuser1' + domain = '00000000-0000-0000-0000-000000000001' + principalName = 'testuser1@domain.com' + mailAddress = 'test.user1@domain.com' + origin = 'aad' + originId = '00000000-0000-0000-0000-000000000001' + displayName = 'User1, Test' + descriptor = 'aad.00000000-0000-0000-0000-000000000001' + isDeletedInOrigin = $null + metaType = 'member' + } + + $mockUser2 = [PSCustomObject]@{ + subjectKind = 'user' + directoryAlias = 'testuser2' + domain = '00000000-0000-0000-0000-000000000002' + principalName = 'testuser2@domain.com' + mailAddress = 'test.user2@domain.com' + origin = 'aad' + originId = '00000000-0000-0000-0000-000000000002' + displayName = 'User2, Test' + descriptor = 'aad.00000000-0000-0000-0000-000000000002' + isDeletedInOrigin = $null + metaType = 'guest' + } + + $mockListResponse = [PSCustomObject]@{ + value = @($mockUser1, $mockUser2) + } + } + + Context 'Core Functionality Tests' { + BeforeEach { + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { return $mockListResponse } + Mock -ModuleName Azure.DevOps.PSModule Confirm-Default { } + Mock -ModuleName Azure.DevOps.PSModule Start-Sleep { } + } + + It 'Should retrieve all users in organization' { + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri + + # Assert + $result | Should -HaveCount 2 + $result[0].displayName | Should -Be 'User1, Test' + $result[1].displayName | Should -Be 'User2, Test' + } + + It 'Should return PSCustomObject with expected properties' { + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri + + # Assert + $result[0].PSObject.Properties.Name | Should -Contain 'subjectKind' + $result[0].PSObject.Properties.Name | Should -Contain 'directoryAlias' + $result[0].PSObject.Properties.Name | Should -Contain 'domain' + $result[0].PSObject.Properties.Name | Should -Contain 'principalName' + $result[0].PSObject.Properties.Name | Should -Contain 'mailAddress' + $result[0].PSObject.Properties.Name | Should -Contain 'origin' + $result[0].PSObject.Properties.Name | Should -Contain 'originId' + $result[0].PSObject.Properties.Name | Should -Contain 'displayName' + $result[0].PSObject.Properties.Name | Should -Contain 'isDeletedInOrigin' + $result[0].PSObject.Properties.Name | Should -Contain 'metaType' + $result[0].PSObject.Properties.Name | Should -Contain 'collectionUri' + } + + It 'Should construct API URI correctly for listing users' { + # Act + Get-AdoUser -CollectionUri $mockCollectionUri + + # Assert + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $Uri -eq "$mockCollectionUri/_apis/graph/users" + } + } + + It 'Should use GET method for API call' { + # Act + Get-AdoUser -CollectionUri $mockCollectionUri + + # Assert + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $Method -eq 'GET' + } + } + + It 'Should use default API version 7.2-preview.1' { + # Act + Get-AdoUser -CollectionUri $mockCollectionUri + + # Assert + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $Version -eq '7.2-preview.1' + } + } + + It 'Should filter users by Name parameter' { + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri -Name 'User1, Test' + + # Assert + $result | Should -HaveCount 1 + $result.displayName | Should -Be 'User1, Test' + } + + It 'Should support wildcard filtering by name' { + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri -Name '*User1*' + + # Assert + $result | Should -HaveCount 1 + $result.displayName | Should -Be 'User1, Test' + } + + It 'Should automatically iterate continuation tokens when listing users' { + # Arrange + $firstPage = [PSCustomObject]@{ + value = @($mockUser1) + continuationToken = 'token123' + } + $secondPage = [PSCustomObject]@{ + value = @($mockUser2) + continuationToken = $null + } + $script:userCallCount = 0 + + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { + $script:userCallCount++ + if ($script:userCallCount -eq 1) { + return $firstPage + } + return $secondPage + } + + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri -SubjectTypes @('aad') + + # Assert + $result | Should -HaveCount 2 + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 2 + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -ParameterFilter { + $QueryParameters -match 'continuationToken=token123' + } + } + } + + Context 'Parameter Set Tests' { + BeforeEach { + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { return $mockUser1 } + Mock -ModuleName Azure.DevOps.PSModule Confirm-Default { } + Mock -ModuleName Azure.DevOps.PSModule Start-Sleep { } + } + + It 'Should retrieve group by descriptor using ByDescriptor parameter set' { + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri -UserDescriptor $mockUserDescriptor + + # Assert + $result | Should -Not -BeNullOrEmpty + $result.descriptor | Should -Be $mockUserDescriptor + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $Uri -eq "$mockCollectionUri/_apis/graph/users/$mockUserDescriptor" + } + } + + It 'Should include scopeDescriptor query parameter when specified' { + # Arrange + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { return $mockListResponse } + + # Act + Get-AdoUser -CollectionUri $mockCollectionUri -ScopeDescriptor $mockScopeDescriptor + + # Assert + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $QueryParameters -match "scopeDescriptor=$mockScopeDescriptor" + } + } + + It 'Should include subjectTypes query parameter' { + # Arrange + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { return $mockListResponse } + + # Act + Get-AdoUser -CollectionUri $mockCollectionUri -SubjectTypes @('aad', 'svc') + + # Assert + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $QueryParameters -match 'subjectTypes=aad,svc' + } + } + + } + + Context 'Pipeline Support Tests' { + BeforeEach { + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { return $mockUser1 } + Mock -ModuleName Azure.DevOps.PSModule Confirm-Default { } + Mock -ModuleName Azure.DevOps.PSModule Start-Sleep { } + } + + It 'Should accept UserDescriptor from pipeline' { + # Arrange + $descriptors = @( + 'aad.00000000-0000-0000-0000-000000000001', + 'aad.00000000-0000-0000-0000-000000000002' + ) + + # Act + $result = $descriptors | Get-AdoUser -CollectionUri $mockCollectionUri + + # Assert + $result | Should -HaveCount 2 + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 2 + } + + It 'Should accept objects with UserDescriptor property from pipeline' { + # Arrange + $objects = @( + [PSCustomObject]@{ UserDescriptor = 'aad.00000000-0000-0000-0000-000000000001' } + [PSCustomObject]@{ UserDescriptor = 'aad.00000000-0000-0000-0000-000000000002' } + ) + + # Act + $result = $objects | Get-AdoUser -CollectionUri $mockCollectionUri + + # Assert + $result | Should -HaveCount 2 + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 2 + } + } + + Context 'Error Handling Tests' { + BeforeEach { + Mock -ModuleName Azure.DevOps.PSModule Confirm-Default { } + Mock -ModuleName Azure.DevOps.PSModule Start-Sleep { } + } + + It 'Should handle InvalidSubjectTypeException gracefully with warning' { + # Arrange + $invalidSubjectError = [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Invalid subject type'), + 'InvalidSubjectTypeException', + [System.Management.Automation.ErrorCategory]::InvalidArgument, + $null + ) + $invalidSubjectError.ErrorDetails = [System.Management.Automation.ErrorDetails]::new('{"message":"InvalidSubjectTypeException: Subject does not exist"}') + + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { throw $invalidSubjectError } + Mock -ModuleName Azure.DevOps.PSModule Write-Warning { } + + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri -ScopeDescriptor 'invalid-scope' -WarningAction SilentlyContinue + + # Assert + $result | Should -BeNullOrEmpty + Should -Invoke Write-Warning -ModuleName Azure.DevOps.PSModule -Times 1 + } + + It 'Should handle GraphSubjectNotFoundException gracefully with warning' { + # Arrange + $notFoundError = [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Subject not found'), + 'GraphSubjectNotFoundException', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + $null + ) + $notFoundError.ErrorDetails = [System.Management.Automation.ErrorDetails]::new('{"message":"GraphSubjectNotFoundException: Group descriptor not found"}') + + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { throw $notFoundError } + Mock -ModuleName Azure.DevOps.PSModule Write-Warning { } + + # Act + $result = Get-AdoUser -CollectionUri $mockCollectionUri -UserDescriptor 'invalid-descriptor' -WarningAction SilentlyContinue + + # Assert + $result | Should -BeNullOrEmpty + Should -Invoke Write-Warning -ModuleName Azure.DevOps.PSModule -Times 1 + } + + It 'Should propagate unexpected errors' { + # Arrange + $genericError = [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Server error'), + 'ServerError', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $null + ) + $genericError.ErrorDetails = [System.Management.Automation.ErrorDetails]::new('{"message":"Internal server error"}') + + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { throw $genericError } + + # Act & Assert + { Get-AdoUser -CollectionUri $mockCollectionUri } | Should -Throw + } + } + + Context 'CollectionUri Handling Tests' { + BeforeEach { + Mock -ModuleName Azure.DevOps.PSModule Invoke-AdoRestMethod { return $mockListResponse } + Mock -ModuleName Azure.DevOps.PSModule Confirm-Default { } + Mock -ModuleName Azure.DevOps.PSModule Start-Sleep { } + } + + It 'Should use environment default CollectionUri when not specified' { + # Arrange + $env:DefaultAdoCollectionUri = 'https://dev.azure.com/default-org' + + # Act + Get-AdoUser + + # Assert + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $Uri -match 'https://vssps.dev.azure.com/default-org' + } + } + + It 'Should work with vssps.dev.azure.com CollectionUri' { + # Arrange + $vsspsUri = 'https://vssps.dev.azure.com/test-org' + + # Act + Get-AdoUser -CollectionUri $vsspsUri + + # Assert + Should -Invoke Invoke-AdoRestMethod -ModuleName Azure.DevOps.PSModule -Times 1 -ParameterFilter { + $Uri -eq "$vsspsUri/_apis/graph/users" + } + } + } +}