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"
+ }
+ }
+ }
+}