diff --git a/.build/tasks/1.PreLoad.ps1 b/.build/tasks/1.PreLoad.ps1 new file mode 100644 index 000000000..dc2ae836a --- /dev/null +++ b/.build/tasks/1.PreLoad.ps1 @@ -0,0 +1,30 @@ +task PreLoad { + + # Write a script to check the PSModule Path and add the output/ + # folder to the PSModule Path + + # Get the output directory + $RepositoryRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent + $outputDir = Join-Path -Path $RepositoryRoot -ChildPath 'output' + $supportingModules = Join-Path -Path $RepositoryRoot -ChildPath 'output/AzureDevOpsDsc/0.0.1/Modules' + + # Test if the output and supporting modules directories exist in the PSModulePath + if (-not $IsWindows) { + $modulelist = ($env:PSModulePath -split ":") + $delimiter = ":" + } else { + $modulelist = ($env:PSModulePath -split ";") + $delimiter = ";" + } + + # Check if the output directory is in the moduleList + if ($moduleList -notcontains $outputDir) { + $env:PSModulePath = "{0}{1}{2}" -f $env:PSModulePath, $delimiter, $outputDir + Write-Host "Adding $outputDir to PSModulePath" + } + if ($moduleList -notcontains $supportingModules) { + $env:PSModulePath = "{0}{1}{2}" -f $env:PSModulePath, $delimiter, $supportingModules + Write-Host "Adding $supportingModules to PSModulePath" + } + +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..96950a109 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/powershell +{ + "name": "PowerShell", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/powershell:lts-debian-11", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "upgradePackages": "false", + "nonFreePackages": "true" + } + }, + + "postCreateCommand": "sudo chsh vscode -s \"$(which pwsh)\"", + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.defaultProfile.linux": "pwsh" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-vscode.powershell" + ] + } + } + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.gitattributes b/.gitattributes index d49b050c9..9f0fb1f3b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,4 @@ *.jpg binary *.xl* binary *.pfx binary +coverage.xml diff --git a/.gitignore b/.gitignore index 3239758a6..8e00f8522 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,14 @@ output/ +.devcontainer/ +internal-tests/ +LCM/Datum/* **.bak *.local.* !**/README.md .kitchen/ +*.clixml *.suo *.user *.coverage @@ -16,3 +20,8 @@ node_modules package-lock.json CodeCoverage.JaCoCo.xml +.vscode/launch.json +Temp/* +TestProject.yml +TestProject2.yml +settings.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d480d6506..d615419d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,14 +22,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 'prefix.ps1' ([issue #12](https://github.com/dsccommunity/AzureDevOpsDsc/issues/12)). - Added pipeline support for publish markdown content to the GitHub repository wiki ([issue #15](https://github.com/dsccommunity/AzureDevOpsDsc/issues/15)). - This will publish the markdown documentation that is generated bu the - build pipeline. + This will publish the markdown documentation that is generated by the build pipeline. - Added new source folder `WikiSource`. Every markdown file in the folder `WikiSource` will be published to the GitHub repository wiki. The markdown file `Home.md` will be updated with the correct module version on each publish to gallery (including preview). +- Added Resources: + - AzDoGroupPermission + - AzDoOrganizationGroup + - AzDoProjectGroup + - AzDoGroupMember + - AzDoGitRepository + - AzDoGitPermission - AzureDevOpsDsc.Common + - Added New-AzDoAuthenticationProvider. This is invoked prior to the resource invocation. - Added 'wrapper' functionality around the [Azure DevOps REST API](https://docs.microsoft.com/en-us/rest/api/azure/devops/) + - Added Supporting Functions for Azure Managed Identity. +- Added Unit Testing to AzureDevOpsDsc.Common + ### Changed @@ -39,12 +49,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 for more information). - Updated pipeline file `RequiredModules.ps1` to latest pipeline pattern. - Updated pipeline file `build.yaml` to latest pipeline pattern. + - Enhanced Authentication Mechanisms. + The classes have been refactored to accommodate a variety of authentication methods. + This refactoring allows the system to support multiple authentication + protocols, enhancing security and providing flexibility in integrating with + different identity providers. + - Added LookupResult Property to classes. A new property, LookupResult, + has been introduced to the classes. This addition enables the classes to + efficiently store and retrieve lookup results, improving data handling + capabilities and streamlining processes that depend on quick access + to these results. + - Added [DSCGetSummaryState] class. : Introduced an additional class, + [DSCGetSummaryState], which serves to represent the changes that have been detected. + - The Get() and Test() methods have undergone a redesign. + The Get-* commands now efficiently retrieve and identify complex changes, + which are then depicted within the [DSCGetSummaryState] class. - AzDevOpsProject - Added a validate set to the parameter `SourceControlType` to (for now) limit the parameter to the values `Git` and `Tfvc`. - Update comment-based help to remove text which the valid values are since that is now add automatically to the documentation (conceptual help and wiki documentation). +- Update build.yaml tests reference: + - Added: ./azuredevopsdsc.common.tests.ps1 + - Added: ./azuredevopsdsc.tests.ps1 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cafa70440..25ce14ea7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,21 +15,23 @@ The `AzureDevOpsDsc` module consists of a few key components: * The nested, `AzureDevOpsDsc.Common` module, which itself, consists of the following sets of functions/commands: - * `Api` - These are used as generic wrappers around the - Azure DevOps REST API), aimed to minimise duplication of functionality and - code across all `Resources`. + * `Api` - These functions serve as generic wrappers around the Azure DevOps REST API, designed to minimize code duplication and functionality overlap across all resources. The private functions directory is organized into the following categories: - * `Connection` - These support connection to the Azure DevOps REST API. + * `Api` - Contains common functions that perform API calls to the Azure DevOps API using `Invoke-AZDORestMethod`. + * `Authentication` - Includes common functions that manage authentication with the Azure DevOps API. + * `Cache` - Houses common functions that handle caching within Azure DevOps. + > **Note:* The `Cache Initialization` directory is an ordered directory used to initialize and populate the cache when the module is loaded. + * `Helper` - Contains common helper functions used by resource functions to perform specific tasks. - * `Resources` - These invoke the `Api` functions/commands to - manage specific, Azure DevOps REST API resources (e.g. `Projects`). + * `Resources` - These invoke the `Api` functions/commands to + manage specific, Azure DevOps REST API resources (e.g. `Projects`). - * `Server` - These are specific to Azure DevOps - **Server** - the self-hosted, typically on-premise, edition of Azure DevOps. + * `Server` - These are specific to Azure DevOps + **Server** - the self-hosted, typically on-premise, edition of Azure DevOps. - * `Services` - These are specific to Azure DevOps - **Services** - the Microsoft, cloud-hosted, 'Software-as-a-Service' (SaaS) - solution. + * `Services` - These are specific to Azure DevOps + **Services** - the Microsoft, cloud-hosted, 'Software-as-a-Service' (SaaS) + solution. The layout/structure of the module and it's resources/components is designed to: @@ -39,12 +41,811 @@ The layout/structure of the module and it's resources/components is designed to: * Minimise the complexity of the DSC Resources themselves, in turn, aiming to: * Reduce the amount of new functionality and effort required to add new, DSC Resources * Increase the reliability and robustness of the DSC Resources -* Allow all the DSC Resources to be able to use independent, Personal Access Tokens - (PATs) to allow distinct PATs, with distinct privileges, to perform distinct - operations (as opposed to requiring a single PAT with excess privileges). --- +## Using the Caching Mechanism + +The caching mechanism implemented in this module leverages CLIXML (Command-Line Interface XML) to store cached content. This approach ensures that the cached data is serialized into a structured XML format. + +### Cache Initialization + +Before the cache can be used, it must first be loaded from disk. The behavior during initialization varies based on the type of cache: + +* **Conditional Clearing:** The decision to clear the cache upon initialization depends on its type. +* **'Live' Prefix:** If the cache name begins with the prefix 'Live', it will be cleared when loaded into memory. This is because this cache type represents the current live environment. + +This method guarantees that caches containing live data are always current, while other cache types maintain their previously stored content. +After the cache is initialized, the `Live*` cache types are populated using functions located in `source\Modules\AzureDevOpsDsc.Common\Api\Functions\Private\Cache\Cache Initialization`. +These functions run sequentially and create or add items as necessary. + +To start the cache process, simply call `New-AzDoAuthenticationProvider`. +For manual initialization: + +```powershell +# Initialize the Cache + +# Initialize the Cache Objects +Get-AzDoCacheObjects | ForEach-Object { + Initialize-CacheObject -CacheType $_ +} + +# Iterate through each of the caching commands and initialize the cache. +Get-Command "AzDoAPI_*" | Where-Object Source -eq 'AzureDevOpsDsc.Common' | ForEach-Object { + . $_.Name -OrganizationName $AzureDevopsOrganizationName -Verbose +} +``` + +### Cache Update Requirements + +Certain resources will need cache updates when their configuration changes. +Please ensure you update the cache accordingly. +If you are dealing with identity-based caches that utilize permissions, it is crucial to include the `ACLIdentity` property attached to the object. +The `ACLIdentity` property is used by the ACL/ACE helper functions to create ACEs/ACLs for the respective security namespace. + +For Example: + +``` PowerShell +$identity = Get-DevOpsDescriptorIdentity @params -SubjectDescriptor $AzDoLiveGroup.value.descriptor + +$ACLIdentity = [PSCustomObject]@{ + id = $identity.id + descriptor = $identity.descriptor + subjectDescriptor = $identity.subjectDescriptor + providerDisplayName = $identity.providerDisplayName + isActive = $identity.isActive + isContainer = $identity.isContainer +} + +$AzDoLiveGroup.value | Add-Member -MemberType NoteProperty -Name 'ACLIdentity' -Value $ACLIdentity + +$cacheParams = @{ + Key = $AzDoLiveGroup.Key + Value = $AzDoLiveGroup + Type = 'LiveGroups' + SuppressWarning = $true +} + +# Add to the cache +Add-CacheItem @cacheParams +``` + +**Note:** This applies only to objects that do not have the `ACLIdentity` property. + +If you are updating the cache after modifying an *existing* object, you can use `Refresh-CacheIdentity` to update the object: + +``` PowerShell + # Update the cache with the new group + Refresh-CacheIdentity -Identity $group -Key $group.principalName -CacheType 'LiveGroups' +``` + +### Adding an Item to the Cache + +To add an item to the cache use the command `Add-CacheItem` to add the item into the cache memory. +> Please note that it only exists in memory. + +``` PowerShell +Add-CacheItem -Key 'KeyName' -Value $Object -Type 'Type' +``` + +### Writing a CacheItem to Disk + +To save the cache to the disk, you can use the `Export-CacheObject` command. +This function writes the specified cache content to a file on your disk. + +Here is an example of how to export the cache to a file: + +```PowerShell +# Export the 'LiveUsers' cache to a file +Export-CacheObject -CacheType 'LiveUsers' -Content $AzDoLiveUsers +``` + +In this example: +- `-CacheType 'LiveUsers'` specifies the type of cache being exported. +- `-Content $AzDoLiveUsers` provides the actual cache data to be written to the disk. + +### Getting a Cache Item + +To retrieve a cache item, you can use either the `Get-CacheItem` or `Find-CacheItem` functions. + +### `Get-CacheItem` + +The `Get-CacheItem` function is used to perform lookups based on the key set within the cache. +This is useful when you know the specific key of the item you want to retrieve. For example: + +```PowerShell +# Check the cache for the group using its key +$livegroup = Get-CacheItem -Key $Key -Type 'LiveGroups' +``` + +### `Find-CacheItem` + +The `Find-CacheItem` function is used to search for calculated properties against a __cache list__. +This is helpful when you need to perform more complex queries, such as checking if an item with certain criteria exists in the cache. For example: + +```PowerShell +# Perform a lookup in the live cache to see if the group has been deleted and recreated +$renamedGroup = $livegroup | Find-CacheItem { $_.originId -eq $livegroup.originId } +``` + +In summary, use `Get-CacheItem` for direct lookups by key and `Find-CacheItem` for more advanced searches based on calculated properties. + +### Adding a new Cache Type + +To introduce a new cache type into the project, several steps must be completed. + +First, find the `Get-AzDoCacheObjects` function and insert the new cache item type into the array. +The `Get-AzDoCacheObjects` function is utilized by components within the module to validate the cache type. +> Note: If this cache item requires updating each time the module loads, prefix it with `'Live*'`. + +``` PowerShell + return @( + 'Project', + 'Team', + 'Group', + 'SecurityDescriptor', + 'LiveGroups', + 'LiveProjects', + 'LiveUsers', + 'LiveGroupMembers', + 'LiveRepositories', + 'LiveServicePrinciples', + 'LiveACLList', + 'LiveProcesses', + 'SecurityNamespaces', + 'NewCacheItem' + ) +``` + +### Getting an Entire Cache List + +There are two ways to retrieve the cache list. +You can retrieve the cache using `Get-CacheObject -CacheType $Type`, or you can directly access the variable in memory by prefixing `AZDO` with the cache type (e.g., `"AZDO$Type"`). + +For example, you can use `Get-CacheObject` to retrieve a list of items from the `LiveGroups` cache: + +``` PowerShell +$AzDoLiveGroups = Get-CacheObject -CacheType 'LiveGroups' +``` + +In this example, you can directly access the variable stored in memory and export its contents to the cache: + +```PowerShell +# Export the contents of the $AzDoLiveProjects variable to the LiveProjects cache +Export-CacheObject -CacheType 'LiveProjects' -Content $AzDoLiveProjects +``` + +This command uses the `Export-CacheObject` cmdlet to save the data from the `$AzDoLiveProjects` variable into the `LiveProjects` cache. +The `-CacheType` parameter specifies the type of cache, while the `-Content` parameter provides the actual data to be cached. + +# Creating your First Resource + +## Resource Template + +Below is a template for creating a class-based DSC resource in PowerShell. + +```PowerShell +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class ClassName : AzDevOpsDscResourceBase +{ + [DscProperty(Key, Mandatory)] + [Alias('Parameter')] + [System.String]$ResourceParameter + + ClassName() + { + $this.Construct() + } + + [ClassName] Get() + { + return [AzDoProject]$($this.GetDscCurrentStateProperties()) + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + # Properties that don't support Set, yet support New/Remove + return @('') + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) { return $properties } + + $properties.ResourceParameter = $CurrentResourceObject.ResourceParameter + $properties.LookupResult = $CurrentResourceObject.LookupResult + $properties.Ensure = $CurrentResourceObject.Ensure + + return $properties + } +} +``` + +## Create the Class Based Resource + +To create a class-based DSC Resource, follow the template provided above. +This will serve as the foundation for defining the resource's properties and methods. + +## Write Documentation for Resource + +### Overview + +When documenting your resource, it is essential to use the Get-Help format to ensure consistency and clarity. +This format helps users understand each property and method of your resource, providing them with the necessary information to utilize it effectively. + +### Naming the Resource + +The resource name must start with the prefix xAzDo, such as AzDoProject. + +### Guidelines for Documentation + +1. **Clear and Concise Information**: + * Ensure that the description of each property and method is straightforward and easy to understand. + * Avoid unnecessary jargon or overly technical language that may confuse the user. + +1. **Detailed Descriptions**: + * Provide detailed explanations for each property and method. + * Explain what each property represents and how each method functions within the resource. + +1. **Examples**: + * Include examples where necessary to illustrate usage. + * Examples should be relevant and demonstrate common use cases to help users understand how to apply the resource in real-world scenarios. + +### Get-Help Format + +Below is a template you can follow to document your resource using the Get-Help format: + +``` PowerShell +<# +# .SYNOPSIS +Briefly describe what the resource does. + +# .DESCRIPTION +Provide a more detailed explanation of the resource, including its purpose and functionality. + +# .PARAMETER +Describe each parameter required by the resource. Include details such as data type, default values, and any constraints. + +# .EXAMPLE +Show an example of how to use the resource. Include both the code and an explanation of what the example demonstrates. + +# .NOTES +Include any additional information that might be relevant, such as author details, version history, or related resources. + +# .LINK +Provide links to any related documentation or external resources. +#> +``` + +### Example Documentation + +Here’s an example of how you might document a sample resource: + +```Powershell +<# +# .SYNOPSIS +This resource manages the configuration of a web server. + +# .DESCRIPTION +The WebServerResource allows administrators to configure various aspects of a web server, including setting up virtual hosts, managing security settings, and configuring modules. + +# .PARAMETER ServerName +Specifies the name of the web server. This is a mandatory parameter. +Type: String +Default value: None + +# .PARAMETER Port +Specifies the port on which the web server listens. +Type: Integer +Default value: 80 + +# .EXAMPLE +PS C:\> Set-WebServerConfiguration -ServerName "MyWebServer" -Port 8080 + +This command configures the web server named 'MyWebServer' to listen on port 8080. + +# .NOTES +Author: Jane Doe +Version: 1.0.0 + +# .LINK +https://docs.example.com/WebServerResource +#> +``` + +By following these guidelines and utilizing the Get-Help format, you can create comprehensive and user-friendly documentation for your resource. + +## Create `Get`, `Test`, `Set`, `New`, `Remove` Functions + +### Lifecycle Management Functions for DSC Resources + +These functions are crucial for managing the lifecycle of your Desired State Configuration (DSC) resource. They are located at: + +`source\Modules\AzureDevOpsDsc.Common\Resources\Functions\Public\ResourceName` + +When creating these functions, make sure to add them to the `FunctionsToExport` section in the file located at: + +`source\Modules\AzureDevOpsDsc.Common\AzureDevOpsDsc.Common.psd1`. + +#### Function Parameters + +The parameters of these functions must be consistent and should reflect the properties of the resource. For instance: + +##### Resource Definition +```PowerShell +class ClassName : AzDevOpsDscResourceBase { + [DscProperty(Key, Mandatory)] + [Alias('Parameter')] + [System.String]$ResourceParameter +} +``` + +##### Get-Function Example +```PowerShell +Function Get-ClassName { + [CmdletBinding()] + param ( + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure + ) +} +``` + +#### Additional Parameters + +Include additional parameters such as `Ensure` and `LookupResult` to ensure comprehensive functionality. +Here's an example: + +```PowerShell +Function Get-ClassName { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ResourceParameter, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure + ) +} +``` + +By following this structure, you ensure that your DSC resource management functions are well-defined and consistent with the resource properties. + +#### The `Get` Function + +The `Get` function performs the lookup of the resource properties and calculates what has changed. +The changed contents are stored within the `LookupResult.PropertiesChanged` property. + +```PowerShell +Function Get-AzDoProjectServices { + + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Repos')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$GitRepositories = 'Enabled', + + [Parameter()] + [Alias('Board')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$WorkBoards = 'Enabled', + + [Parameter()] + [Alias('Pipelines')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$BuildPipelines = 'Enabled', + + [Parameter()] + [Alias('Tests')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$TestPlans = 'Enabled', + + [Parameter()] + [Alias('Artifacts')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$AzureArtifact = 'Enabled', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # + # Construct a hashtable detailing the group + + $Result = @{ + #Reasons = $() + Ensure = [Ensure]::Absent + propertiesChanged = @() + status = [DSCGetSummaryState]::Unchanged + } + + # + # Attempt to retrive the Project from the Live Cache. + Write-Verbose "[Get-AzDevOpsProjectServices] Retriving the Project from the Live Cache." + + # Retrive the Repositories from the Live Cache. + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + # If the Project does not exist in the Live Cache, return the Project object. + if ($null -eq $Project) { + Write-Warning "[Get-AzDevOpsProjectServices] The Project '$ProjectName' was not found in the Live Cache." + $Result.Status = [DSCGetSummaryState]::NotFound + return $Result + } + + $params = @{ + Organization = $Global:DSCAZDO_OrganizationName + ProjectId = $Project.id + } + + # Enumerate the Project Services. + $Result.LiveServices = @{ + Repos = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Repos + Boards = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Boards + Pipelines = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Pipelines + Tests = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_TestPlans + Artifacts = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Artifacts + } + + # Compare the Project Services with the desired state. + if ($GitRepositories -ne $Result.LiveServices.Repos.state) { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $GitRepositories + FeatureId = $LocalizedDataAzURLParams.ProjectService_Repos + } + } + if ($WorkBoards -ne $Result.LiveServices.Boards.state) { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $WorkBoards + FeatureId = $LocalizedDataAzURLParams.ProjectService_Boards + } + } + if ($BuildPipelines -ne $Result.LiveServices.Pipelines.state) { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $BuildPipelines + FeatureId = $LocalizedDataAzURLParams.ProjectService_Pipelines + } + } + if ($TestPlans -ne $Result.LiveServices.Tests.state) { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $TestPlans + FeatureId = $LocalizedDataAzURLParams.ProjectService_TestPlans + } + } + if ($AzureArtifact -ne $Result.LiveServices.Artifacts.state) { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $AzureArtifact + FeatureId = $LocalizedDataAzURLParams.ProjectService_Artifacts + } + } + + return $Result + +} +``` + +### The `LookupResult` Property + +The `LookupResult` property holds the results of the resource lookup and any changes detected. + +Example: + +```PowerShell +# Enumerate the Project Services. +$Result.LiveServices = @{ + Repos = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Repos + Boards = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Boards + Pipelines = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Pipelines + Tests = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_TestPlans + Artifacts = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Artifacts +} + +# Compare the Project Services with the desired state. +if ($GitRepositories -ne $Result.LiveServices.Repos.state) { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $GitRepositories + FeatureId = $LocalizedDataAzURLParams.ProjectService_Repos + } +} +``` + +This example demonstrates how to enumerate project services and compare them against the desired state, updating the `LookupResult` property accordingly. + +#### The `Status` Property + +The `Status` property provides detailed information about the changes identified by the `get` method. +This property indicates the specific status of the item being evaluated, allowing for a clear understanding of its current state. + +#### Possible Status Values + +* **Changed**: Indicates that the item has undergone modifications. +* **Unchanged**: Signifies that the item remains in its original state with no alterations. +* **NotFound**: Denotes that the item could not be located within AZDO. +* **Renamed**: Implies that the item has been renamed from its original identifier. +* **Missing**: Suggests that the item is absent or has been deleted within AZDO. + +Each status value aids in classifying the result of the `get` method, offering clear insights into the changes affecting the item. +This information is utilized by the `Test` method, along with the `Ensure` enum, to determine the appropriate course of action. + +For Example: + +``` PowerShell +# +# Construct a hashtable detailing the group +$result = @{ + Ensure = [Ensure]::Absent + ProjectName = $ProjectName + # Other Key/Value items + propertiesChanged = @() + status = $null +} + +# Test if the project exists. If the project does not exist, return NotFound +if (($null -eq $project) -and ($null -ne $ProjectName)) +{ + $result.Status = [DSCGetSummaryState]::NotFound + return $result +} +``` + +#### Making API Calls + +API calls are not made directly within the Get, Set, Test, New, and Remove functions. +Instead, they are abstracted to the `source\Modules\AzureDevOpsDsc.Common\Api\Functions\Private\Api` directory. +This directory contains functions that handle API calls to the respective endpoints. + +Here are some sample code snippets: + +``` PowerShell +Write-Verbose "[New-GitRepository] Creating new repository '$($RepositoryName)' in project '$($Project.name)'" + +# Define parameters for creating a new DevOps group +$params = @{ + ApiUri = '{0}/{1}/_apis/git/repositories?api-version={2}' -f $ApiUri, $Project.name, $ApiVersion + Method = 'POST' + ContentType = 'application/json' + Body = @{ + name = $RepositoryName + project = @{ + id = $Project.id + } + } | ConvertTo-Json +} + +# Try to invoke the REST method to create the group and return the result +try { + $repo = Invoke-AzDevOpsApiRestMethod @params + Write-Verbose "[New-GitRepository] Repository Created: '$($repo.name)'" + return $repo +} +# Catch any exceptions and write an error message +catch { + Write-Error "[New-GitRepository] Failed to Create Repository: $_" +} +``` + +When writing API calls, utilize `Invoke-AzDevOpsApiRestMethod` to execute the call. +This function will automatically inject authentication headers, manage pagination, and handle rate-limiting. + +# Integrating Enhanced Authentication Mechanisms + +With the continuous release of new technologies by Microsoft, this module is engineered to support a variety of advanced authentication mechanisms. +This ensures compatibility with the latest security standards and provides flexibility in choosing the most suitable authentication method for your needs. + +Important: The current authentication mechanism depends on the Authorization HTTP header. + +This means that all authentication requests must include the appropriate credentials within the Authorization header to ensure secure access. +This method is crucial for maintaining the integrity and confidentiality of the data being transmitted. + +## 1. Create an Authentication Class + +To create an authentication class, you need to add a new class within the `source\Classes` directory. +Ensure that this class is declared before the `[DSCResourceBase]` class. +Additionally, include a global function named `New-ClassName` in this class, which will be invoked by nested modules. + +> **Important:** It's acceptable to adjust the execution order to accommodate the class. + +### Implementation Template + +Below is a template for creating a sample authentication class called `SampleAuthenticationType`. +This class inherits from the `AuthenticationToken` base class and includes methods for validating tokens, checking expiration, and retrieving the access token. +The global function `New-SampleAuthenticationType` is also provided to instantiate the class. + +```PowerShell +# Define the SampleAuthenticationType class +Class SampleAuthenticationType : AuthenticationToken { + + # Property to store the value of the authentication token + [Property]$Value + + # Constructor to initialize the SampleAuthenticationType instance + SampleAuthenticationType() { + } + + # Hidden function to validate the ManagedIdentityTokenObj + Hidden [Bool]isValid($ManagedIdentityTokenObj) { + } + + # Function to check if the token has expired + [Bool]isExpired() { + } + + # Function to return the access token as a string + [String] Get() { + + # Test the caller + $this.TestCaller() + + # Return the access token + return ($this.ConvertFromSecureString($this.access_token)) + + } +} + +# Global function to create a new SampleAuthenticationType object +Function global:New-SampleAuthenticationType ([PSCustomObject]$Obj) { + # Create and return a new SampleAuthenticationType object + return [SampleAuthenticationType]::New($Obj) +} +``` + +### Detailed Explanation + +#### Class Definition + +* **Class Declaration**: The `SampleAuthenticationType` class is defined and inherits from the `AuthenticationToken` base class. + +* **Properties**: + * `$Value`: A property to store the value of the authentication token. + +* **Constructor**: + * `SampleAuthenticationType()`: A constructor method to initialize instances of the `SampleAuthenticationType` class. + +* **Methods**: + * `isValid($ManagedIdentityTokenObj)`: A hidden method to validate the provided managed identity token object. + * `isExpired()`: A method to check if the current token has expired. + * `Get()`: A method to return the access token as a string. This is used by the Modules + + > **IMPORTANT**: All tokens must be stored as `[SecureString]` in memory to prevent accidental leakage. Additionally, the `Get()` method can only be invoked by approved functions/methods to further mitigate the risk. + +#### Global Function + +* **New-SampleAuthenticationType**: + * A global function that creates and returns a new instance of the `SampleAuthenticationType` class using a provided `PSCustomObject`. + +### Usage Example + +Here is an example of how to use the `SampleAuthenticationType` class and its associated global function: + +```PowerShell +# Create a PSCustomObject with necessary properties +$customObject = [PSCustomObject]@{ + Property1 = "Value1" + Property2 = "Value2" +} + +# Create a new SampleAuthenticationType object +$authToken = New-SampleAuthenticationType -Obj $customObject + +# Check if the token is valid +$isValid = $authToken.isValid($managedIdentityTokenObj) + +# Check if the token is expired +$isExpired = $authToken.isExpired() + +# Get the access token +$accessToken = $authToken.Get() +``` + +By following this template and detailed explanation, you can create a robust authentication class that integrates seamlessly with your PowerShell DSC resources and nested modules. + +## 2. Integrate the Authentication Mechanism into `New-AzDoAuthenticationProvider` + +In this step, you need to incorporate the authentication mechanism within the `New-AzDoAuthenticationProvider` function. +Ensure that the necessary authentication protocols are properly implemented to facilitate secure access. + +## 3. Integrate Changes into `Add-AuthenticationHTTPHeader` + +Next, apply the changes to the `Add-AuthenticationHTTPHeader` function. +This involves updating the function to include the new authentication headers, ensuring that each HTTP request is authenticated correctly. + +# Enable Verbose Logging + +> **IMPORTANT**: Enabling verbose logging will impact performance. + +To enable `Write-Verbose` logging, follow these steps: + +1. **Create a System Environment Variable**: + * Name the variable `AZDO_VERBOSELOGGING_FILEPATH`. + * Set the value of this variable to the desired file path where you want the verbose logs to be stored. + +1. **Steps to Create the Environment Variable**: + * **Windows**: + 1. Open the Start Menu and search for "Environment Variables". + 1. Select "Edit the system environment variables". + 1. In the System Properties window, click on the "Environment Variables" button. + 1. In the Environment Variables window, under the "System variables" section, click "New". + 1. Enter `AZDO_VERBOSELOGGING_FILEPATH` as the variable name. + 1. Enter the full path of the file where you want the logs to be saved as the variable value (e.g., `C:\Logs\AzDoVerbose.log`). + 1. Click "OK" to save the new variable. + 1. Click "OK" again to close the Environment Variables window, and then "OK" to close the System Properties window. + + * **Linux/macOS**: + 1. Open a terminal window. + 1. Edit your shell profile file (e.g., `~/.bashrc`, `~/.zshrc`) using a text editor. + 1. Add the following line to set the environment variable: + ```sh + export AZDO_VERBOSELOGGING_FILEPATH="/path/to/your/logfile.log" + ``` + 1. Save the file and close the text editor. + 1. Source the profile file to apply the changes: + ```sh + source ~/.bashrc # or source ~/.zshrc depending on your shell + ``` + +1. **Verify the Environment Variable**: + * To ensure that the environment variable is set correctly, you can use the following command: + * **Windows** (Command Prompt): + ```cmd + echo %AZDO_VERBOSELOGGING_FILEPATH% + ``` + * ***Windows** (PowerShell): + ```powershell + $env:AZDO_VERBOSELOGGING_FILEPATH + ``` + * **Linux/macOS**: + ```sh + echo $AZDO_VERBOSELOGGING_FILEPATH + ``` + +By setting the `AZDO_VERBOSELOGGING_FILEPATH` environment variable, you direct the `Write-Verbose` output to the specified file, enabling detailed logging for troubleshooting and monitoring purposes. + +Below is an example of the `Write-Verbose` logfile: + +``` Text +[2024-08-13 14:10:45] [Invoke-AzDevOpsApiRestMethod] Invoking the Azure DevOps API REST method 'Get'. +[2024-08-13 14:10:45] [Invoke-AzDevOpsApiRestMethod] API URI: https://vssps.dev.azure.com/sample/_apis/graph/Memberships/vssgp.string?direction=down +[2024-08-13 14:10:45] [Add-AuthenticationHTTPHeader] Adding Managed Identity Token to the HTTP Headers. +[2024-08-13 14:10:45] [Add-AuthenticationHTTPHeader] Adding Header +[2024-08-13 14:10:45] [Invoke-AzDevOpsApiRestMethod] No continuation token found. Breaking loop. +[2024-08-13 14:10:45] No members found for group '[TEAM FOUNDATION]\Enterprise Service Accounts'; skipping. +``` + +# Tests + ## Running the Tests If want to know how to run this module's tests you can look at the [Testing Guidelines](https://dsccommunity.org/guidelines/testing-guidelines/#running-tests) diff --git a/README.md b/README.md index f6a79d298..842236cb7 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ configuration of Azure DevOps and Azure DevOps Server. This project has adopted this [Code of Conduct](CODE_OF_CONDUCT.md). +## Usage + +Please review the following [Usage Documentation](USAGE.md) + ## Releases For each merge to the branch `main` a preview release will be diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 000000000..4fa772c7b --- /dev/null +++ b/USAGE.md @@ -0,0 +1,209 @@ +# Usage Documentation + +This document provides detailed instructions on how to use the module effectively. + +## Prerequisites + +Ensure you have the following prerequisites before proceeding: + +- **PowerShell 7.0** +- **Required Modules**: + - `ChangelogManagement` + - `Configuration` + - `DscResource.AnalyzerRules` + - `DscResource.Common` + - `DscResource.DocGenerator` + - `DscResource.Test` + - `InvokeBuild` + - `MarkdownLinkCheck` + - `Metadata` + - `ModuleBuilder` + - `Pester` + - `Plaster` + - `PSDepend` + - `PSDscResources` + - `PSScriptAnalyzer` + - `Sampler` + - `xDSCResourceDesigner` + +__Using Install-Module__ + +``` PowerShell +# Run as Administrator +Install-Module -Scope AllUsers -Name @( + 'ChangelogManagement' + 'Configuration' + 'DscResource.AnalyzerRules' + 'DscResource.Common' + 'DscResource.DocGenerator' + 'DscResource.Test' + 'InvokeBuild' + 'MarkdownLinkCheck' + 'Metadata' + 'ModuleBuilder' + 'Pester' + 'Plaster' + 'PSDepend' + 'PSDscResources' + 'PSScriptAnalyzer' + 'Sampler' + 'xDSCResourceDesigner' +) +``` + +__Using Install-PSResource__ + +``` PowerShell +# Run as Administrator +Install-PSResource @( + 'ChangelogManagement' + 'Configuration' + 'DscResource.AnalyzerRules' + 'DscResource.Common' + 'DscResource.DocGenerator' + 'DscResource.Test' + 'InvokeBuild' + 'MarkdownLinkCheck' + 'Metadata' + 'ModuleBuilder' + 'Pester' + 'Plaster' + 'PSDepend' + 'PSDscResources' + 'PSScriptAnalyzer' + 'Sampler' + 'xDSCResourceDesigner' +) +``` + +### *AZDODSC_CACHE_DIRECTORY* Environment Variable + +The system environment variable `AZDODSC_CACHE_DIRECTORY` is used by the module +to store caching settings and the cache itself. Make sure this variable is properly +set up in your system environment. + +### *AZDO_WARNINGLOGGING_FILEPATH* and *AZDO_ERRORLOGGING_FILEPATH* Environment Variables + +The `AZDO_WARNINGLOGGING_FILEPATH` and `AZDO_ERRORLOGGING_FILEPATH`environment +variables are typically used in Azure DevOps (AzDO) pipelines or custom scripts +to specify file paths where warning and error logs should be stored. +These variables help manage logging by directing different types of log messages +to separate files, aiding in better organization and analysis. + +#### Usage + +- **AZDO_WARNINGLOGGING_FILEPATH**: This variable defines the path to a file +where all warnings generated during the execution of a pipeline or script will +be logged. It's useful for tracking non-critical issues that may need attention +but do not halt the execution. + +- **AZDO_ERRORLOGGING_FILEPATH**: This variable specifies the path to a file +designated for logging errors. Errors are typically more severe than warnings +and might require immediate action or investigation. + +## Setting Up Managed Identity + +Please use the following [documentation](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops) as a guide to create a managed identity in Azure DevOps. + +## Authentication + +Prior to accessing any resources, it is necessary to configure authentication by utilizing the `New-AzDoAuthenticationProvider` cmdlet. + +```powershell +New-AzDoAuthenticationProvider -OrganizationName $AzureDevopsOrganizationName -UseManagedIdentity +``` + +## Sample Invocation + +Here is an example of how to invoke a resource using the module: + +1. Import the necessary modules: + + ```powershell + Import-Module "\AzureDevOpsDsc\0.0.1\Modules\DscResource.Common\0.17.1\DscResource.Common.psd1" + Import-Module "\AzureDevOpsDsc\0.0.1\Modules\AzureDevOpsDsc.Common\AzureDevOpsDsc.Common.psd1" + Import-Module "\AzureDevOpsDsc\0.0.1\AzureDevOpsDsc.psd1" + ``` + +1. Create a Managed Identity Token: + + ```powershell + New-AzDoAuthenticationProvider -OrganizationName "akkodistestorg" -UseManagedIdentity + ``` + +1. Define the properties to be used by the module: + + ```powershell + $properties = @{ + ProjectName = 'UpdateSharePoint' + GroupName = 'TESTGROUP5' + } + ``` + +1. Invoke the DSC Resource: + + ```powershell + Invoke-DscResource -Name 'AzDoProjectGroup' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' + ``` + +By following these steps, you can successfully set up and use the module with Azure DevOps. + +## Implementation using `AzDO-DSC-LCM` + +[Current Source](https://github.com/ZanattaMichael/AzDO-DSC-LCM) + +This module includes a custom Local Configuration Manager (LCM) built on Datum. By utilizing YAML resource files, similar to Ansible playbooks, administrators can manage their environment using Configuration as Code (CaC). + +Below is an example of how you can define parameters, variables, and resources in a YAML file to manage your Azure DevOps environment: + +**FileName: ProjectPolicies\Project.yml** +```yaml +parameters: {} + +variables: { +} + +resources: + + - name: Project + type: AzureDevOpsDsc/AzDoProject + properties: + projectName: $ProjectName + projectDescription: $ProjectDescription + visibility: private + SourceControlType: Git + ProcessTemplate: Agile +``` + +**FileName: AllNodes\SampleProject\Project.yml** +```yaml +parameters: {} + +variables: + ProjectName: SampleProject + ProjectDescription: 'Never gonna give you up, never gonna let you down!' + +resources: + - name: Project Services + type: AzureDevOpsDsc/AzDoProjectServices + dependsOn: + - AzureDevOpsDsc/AzDoProject/Project + properties: + projectName: $ProjectName + BuildPipelines: disabled + AzureArtifact: disabled +``` + +### Explanation + +- **Parameters**: This section is reserved for any input parameters that the configuration might require. +- **Variables**: Here, you can define reusable variables such as `ProjectName` and `ProjectDescription`. +- **Resources**: This section defines the actual resources to be managed. In this example, we have a resource named "Project Services" of type `AzureDevOpsDsc/AzDoProjectServices`. + +#### Resource Properties + +- `projectName`: Uses the variable `$ProjectName` defined earlier. +- `BuildPipelines`: Set to `disabled`. +- `AzureArtifact`: Set to `disabled`. + +The `dependsOn` attribute ensures that the "Project Services" resource will only be configured after the `AzureDevOpsDsc/AzDoProject/Project` resource has been set up. diff --git a/azuredevopsdsc.common.tests.ps1 b/azuredevopsdsc.common.tests.ps1 new file mode 100644 index 000000000..782a3db3c --- /dev/null +++ b/azuredevopsdsc.common.tests.ps1 @@ -0,0 +1,43 @@ +[CmdletBinding()] +param ( + [Parameter()] + [switch] + $LoadModulesOnly +) + +# Unload the $Global:RepositoryRoot and $Global:TestPaths variables +Remove-Variable -Name RepositoryRoot -Scope Global -ErrorAction SilentlyContinue + +# Set the $Global:RepositoryRoot and $Global:TestPaths variables +$Global:RepositoryRoot = $PSScriptRoot + +Import-Module -Name (Join-Path -Path $Global:RepositoryRoot -ChildPath '/tests/Unit/Modules/TestHelpers/CommonTestCases.psm1') +Import-Module -Name (Join-Path -Path $Global:RepositoryRoot -ChildPath '/tests/Unit/Modules/TestHelpers/CommonTestHelper.psm1') +Import-Module -Name (Join-Path -Path $Global:RepositoryRoot -ChildPath '/tests/Unit/Modules/TestHelpers/CommonTestFunctions.psm1') + +if ($LoadModulesOnly.IsPresent) +{ + return +} + +$config = New-PesterConfiguration + +$config.Run.Path = ".\tests\Unit\Modules\AzureDevOpsDsc.Common" +$config.Output.CIFormat = "GitHubActions" +#$config.Output.Verbosity = "Detailed" +$config.CodeCoverage.Enabled = $true +$config.CodeCoverage.Path = @( + '.\source\Modules\AzureDevOpsDsc.Common\Api', + '.\source\Modules\AzureDevOpsDsc.Common\Connection', + '.\source\Modules\AzureDevOpsDsc.Common\en-US', + '.\source\Modules\AzureDevOpsDsc.Common\LocalizedData', + '.\source\Modules\AzureDevOpsDsc.Common\Resources', + '.\source\Modules\AzureDevOpsDsc.Common\Services' + ) +$config.CodeCoverage.OutputFormat = 'CoverageGutters' +$config.CodeCoverage.OutputPath = ".\output\AzureDevOpsDsc.Common.codeCoverage.xml" +$config.CodeCoverage.OutputEncoding = 'utf8' + +# Get the path to the function being tested + +Invoke-Pester -Configuration $config diff --git a/azuredevopsdsc.tests.ps1 b/azuredevopsdsc.tests.ps1 new file mode 100644 index 000000000..2c85f574b --- /dev/null +++ b/azuredevopsdsc.tests.ps1 @@ -0,0 +1,87 @@ +[CmdletBinding()] +param ( + [Parameter()] + [switch] + $LoadModulesOnly +) + +# Unload the $Global:RepositoryRoot and $Global:TestPaths variables +Remove-Variable -Name RepositoryRoot -Scope Global -ErrorAction SilentlyContinue + +# Set the $Global:RepositoryRoot and $Global:TestPaths variables +$Global:RepositoryRoot = $PSScriptRoot +$ClassesDirectory = "$Global:RepositoryRoot\source\Classes" +$EnumsDirectory = "$Global:RepositoryRoot\source\Enum" +$PublicDirectory = "$Global:RepositoryRoot\source\Modules\AzureDevOpsDsc.Common\Resources\Functions\Public" +$Global:ClassesLoaded = $true + +# +# Load the Helper Modules +Import-Module -Name (Join-Path -Path $Global:RepositoryRoot -ChildPath 'tests\Unit\Modules\TestHelpers\CommonTestFunctions.psm1') + + +# +# Load all the Enums + +Get-ChildItem -LiteralPath $EnumsDirectory -File | ForEach-Object { + Write-Verbose "Dot Sourcing $($_.FullName)" + . $_.FullName +} + +# +# Load all the Classes + +Get-ChildItem -LiteralPath $ClassesDirectory -File | ForEach-Object { + + Write-Verbose "Dot Sourcing $($_.FullName)" + # Read the file and remove [DscResource()] attribute + $file = Get-Command $_.FullName + # Remove [DscResource()] attribute + $content = $file.ScriptContents -replace '\[DscResource\(\)\]', '' + # Convert the string array into ScriptBlock + $scriptBlock = [ScriptBlock]::Create($content) + # Dot source the script block + . $scriptBlock + +} + +# Load all the Helper Functions from the AzureDevOpsDsc.Common Module into Memory +Get-ChildItem -LiteralPath "$($Global:RepositoryRoot)\source\Modules\AzureDevOpsDsc.Common\Api\Functions\Private\Helper" -File -Recurse -Filter *.ps1 | ForEach-Object { + Write-Verbose "Dot Sourcing $($_.FullName)" + . $_.FullName +} + +# Load all the Public Functions from the AzureDevOpsDsc.Common Module into Memory +Get-ChildItem -LiteralPath $PublicDirectory -File -Recurse -Filter *.ps1 | ForEach-Object { + Write-Verbose "Dot Sourcing $($_.FullName)" + . $_.FullName +} + + +if ($LoadModulesOnly.IsPresent) +{ + return +} + +$config = New-PesterConfiguration + +$config.Run.Path = ".\tests\Unit\Classes" +$config.Output.CIFormat = "GitHubActions" +#$config.Output.Verbosity = "Detailed" +$config.CodeCoverage.Enabled = $true +$config.CodeCoverage.Path = @( + '.\source\Classes\' + ) +$config.CodeCoverage.OutputFormat = 'CoverageGutters' +$config.CodeCoverage.OutputPath = ".\output\AzureDevOpsDsc.codeCoverage.xml" +$config.CodeCoverage.OutputEncoding = 'utf8' + +# Get the path to the function being tested + +Invoke-Pester -Configuration $config + + + + + + diff --git a/build.yaml b/build.yaml index 46731b217..18816b55e 100644 --- a/build.yaml +++ b/build.yaml @@ -31,6 +31,7 @@ BuildWorkflow: - test build: + - PreLoad - Clean - Build_Module_ModuleBuilder - Build_NestedModules_ModuleBuilder @@ -46,6 +47,7 @@ BuildWorkflow: - DscResource_Tests_Stop_On_Fail test: + - PreLoad - Pester_Tests_Stop_On_Fail - Pester_if_Code_Coverage_Under_Threshold @@ -64,7 +66,8 @@ Pester: - Modules/DscResource.Common Script: # Only run on unit test on './build.ps1 -Task test' - - tests/Unit + - ./azuredevopsdsc.common.tests.ps1 + - ./azuredevopsdsc.tests.ps1 ExcludeTag: Tag: CodeCoverageThreshold: 40 diff --git a/source/AzureDevOpsDsc.psd1 b/source/AzureDevOpsDsc.psd1 index b99801be9..ed1639871 100644 --- a/source/AzureDevOpsDsc.psd1 +++ b/source/AzureDevOpsDsc.psd1 @@ -2,7 +2,7 @@ RootModule = 'AzureDevOpsDsc.psm1' # Version number of this module. - moduleVersion = '0.0.0' + moduleVersion = '0.0.1' # ID used to uniquely identify this module GUID = '3f8bbada-0fa9-4d80-b3d8-f019c3c60230' @@ -20,16 +20,16 @@ Description = 'Module with DSC Resources for deployment and configuration of Azure DevOps Server/Services.' # Minimum version of the Windows PowerShell engine required by this module - PowerShellVersion = '5.0' + PowerShellVersion = '7.0' # Minimum version of the common language runtime (CLR) required by this module CLRVersion = '4.0' # Functions to export from this module - FunctionsToExport = @() + #FunctionsToExport = @() # Cmdlets to export from this module - CmdletsToExport = @() + #CmdletsToExport = @() # Variables to export from this module VariablesToExport = @() @@ -40,7 +40,11 @@ # Import all the 'DSCClassResource', modules as part of this module NestedModules = @() - DscResourcesToExport = @('AzDevOpsProject') + DscResourcesToExport = @( + 'AzDevOpsProject', + 'AzDoOrganizationGroup', + 'AzDoProjectGroup' + ) RequiredAssemblies = @() diff --git a/source/Classes/001.AuthenticationToken.ps1 b/source/Classes/001.AuthenticationToken.ps1 new file mode 100644 index 000000000..80729f76e --- /dev/null +++ b/source/Classes/001.AuthenticationToken.ps1 @@ -0,0 +1,128 @@ +<# + .SYNOPSIS + Represents an authentication token with methods to manage and retrieve the token securely. + + .DESCRIPTION + The AuthenticationToken class encapsulates an authentication token, providing methods to convert a SecureString to a String, + test the call stack for specific functions, and ensure that the Get() method is called only by authorized functions. + + .PROPERTIES + [TokenType]$tokenType + The type of the token. + + hidden [bool]$linux + Indicates if the environment is Linux. + + hidden [SecureString]$access_token + The secure access token. + + .METHODS + hidden [String] ConvertFromSecureString([SecureString]$SecureString) + Converts a SecureString to a plain String. + + hidden [Bool] TestCallStack([String]$name) + Tests the call stack to check if a specific function is in the call stack. + + TestCaller() + Ensures that the Get() method is called only by authorized functions and not within certain contexts. + + [String] Get() + Retrieves the access token after ensuring that the calling function is authorized to do so. + + .NOTES + The class is designed to prevent unauthorized access to the access token and to ensure that the token is handled securely. +#> + +class AuthenticationToken +{ + [TokenType] $tokenType + hidden [bool] $linux = $isLinux + hidden [SecureString] $access_token + + # Function to convert a SecureString to a String + hidden [String] ConvertFromSecureString([SecureString] $SecureString) + { + # Convert a SecureString to a String + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + $plainTextString = $( + if ($this.linux) + { + [System.Runtime.InteropServices.Marshal]::PtrToStringUni($BSTR) + } else { + [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + } + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + ) + return $plainTextString + + } + + # Function to test the call stack + hidden [Bool] TestCallStack([String] $name) + { + # Get the call stack + Write-Verbose "[AuthenticationToken] Getting the call stack." + $CallStack = Get-PSCallStack + + # Check if any of the callers in the call stack is Invoke-DSCResource + foreach ($stackFrame in $CallStack) + { + if ($stackFrame.Command -eq $name) + { + Write-Verbose "[AuthenticationToken] The calling function is $name." + return $true + } + } + return $false + } + + # Function to prevent unauthorized access to the Get() method + TestCaller() + { + # Prevent Execution and Writing to Files and Pipeline Variables. + + <# + The Get() method can only be called within the following functions: + - Add-AuthenticationHTTPHeader + - Invoke-AzDevOpsApiRestMethod + - New-AzDoAuthenticationProvider + #> + if ( + (-not($this.TestCallStack('Add-AuthenticationHTTPHeader'))) -and + (-not($this.TestCallStack('Invoke-AzDevOpsApiRestMethod'))) -and + (-not($this.TestCallStack('New-AzDoAuthenticationProvider'))) + ) + { + # Token can only be called within Invoke-AzDevOpsApiRestMethod. Test to see if the calling function is Invoke-AzDevOpsApiRestMethod + throw "[AuthenticationToken][Access Denied] The Get() method can only be called within AzureDevOpsDsc.Common." + } + + # Token cannot be returned within a Write-* function. Test to see if the calling function is Write-* + if ($this.TestCallStack('Write-')) + { + throw "[AuthenticationToken][Access Denied] The Get() method cannot be called within a Write-* function." + } + + # Token cannot be written to a file. Test to see if the calling function is Out-File + if ($this.TestCallStack('Out-File')) + { + throw "[AuthenticationToken][Access Denied] The Get() method cannot be called within Out-File." + } + } + + # Return the access token + [String] Get() + { + # Verbose output + Write-Verbose "[AuthenticationToken] Getting the access token:" + Write-Verbose "[AuthenticationToken] Ensuring that the calling function is allowed to call the Get() method." + + # Test the caller + $this.TestCaller() + + Write-Verbose "[AuthenticationToken] Token Retrieval Successful." + + # Return the access token + return ($this.ConvertFromSecureString($this.access_token)) + } +} diff --git a/source/Classes/002.PersonalAccessToken.ps1 b/source/Classes/002.PersonalAccessToken.ps1 new file mode 100644 index 000000000..85bd85c5c --- /dev/null +++ b/source/Classes/002.PersonalAccessToken.ps1 @@ -0,0 +1,99 @@ + + +<# +.SYNOPSIS +Represents a Personal Access Token (PAT) used for authentication. + +.DESCRIPTION +The `PersonalAccessToken` class inherits from the `AuthenticationToken` class and provides methods to handle Personal Access Tokens. +It includes constructors for initializing the token using a plain text string or a secure string, and a method to check if the token is expired. + +.CONSTRUCTORS +PersonalAccessToken([String]$PersonalAccessToken) + Initializes a new instance of the `PersonalAccessToken` class using a plain text string. + +PersonalAccessToken([SecureString]$SecureStringPersonalAccessToken) + Initializes a new instance of the `PersonalAccessToken` class using a secure string. + +.METHODS +[Bool]isExpired() + Checks if the Personal Access Token is expired. Always returns $false as Personal Access Tokens do not expire. + +.NOTES +The `PersonalAccessToken` class sets the token type to `PersonalAccessToken` and converts the plain text token to a secure string if necessary. +#> +class PersonalAccessToken : AuthenticationToken +{ + + PersonalAccessToken([String]$PersonalAccessToken) + { + $this.tokenType = [TokenType]::PersonalAccessToken + $this.access_token = ConvertTo-Base64String -InputObject ":$($PersonalAccessToken)" | ConvertTo-SecureString -AsPlainText -Force + } + + PersonalAccessToken([SecureString]$SecureStringPersonalAccessToken) + { + $this.tokenType = [TokenType]::PersonalAccessToken + $this.access_token = $SecureStringPersonalAccessToken + } + + [Bool]isExpired() { + + # Personal Access Tokens don't contain expiry information. Without performing a global lookup of PAT tokens, + # we can't determine if a PAT is expired Therefore, we always return $false. + + return $false + } + +} + +<# +Creates a new PersonalAccessToken object. + +.DESCRIPTION +This function creates a new PersonalAccessToken object using either a plain text personal access token or a secure string personal access token. + +.PARAMETER PersonalAccessToken +A plain text personal access token. + +.PARAMETER SecureStringPersonalAccessToken +A secure string personal access token. + +.RETURNS +Returns a new instance of the PersonalAccessToken object. + +.EXAMPLE +$token = New-PersonalAccessToken -PersonalAccessToken "your-token-here" +Creates a new PersonalAccessToken object using a plain text token. + +.EXAMPLE +$secureToken = ConvertTo-SecureString "your-token-here" -AsPlainText -Force +$token = New-PersonalAccessToken -SecureStringPersonalAccessToken $secureToken +Creates a new PersonalAccessToken object using a secure string token. + +.NOTES +If neither a plain text personal access token nor a secure string personal access token is provided, an error is thrown. +#> + +Function global:New-PersonalAccessToken ([String]$PersonalAccessToken, [SecureString]$SecureStringPersonalAccessToken) +{ + + # Verbose output + Write-Verbose "[PersonalAccessToken] Creating a new ManagedIdentityToken object." + + if ($PersonalAccessToken) + { + # Create a new PersonalAccessToken object + return [PersonalAccessToken]::New($PersonalAccessToken) + } + elseif ($SecureStringPersonalAccessToken) + { + # Create a new PersonalAccessToken object + return [PersonalAccessToken]::New($SecureStringPersonalAccessToken) + } + else + { + throw "Error. A Personal Access Token or SecureString Personal Access Token must be provided." + } + +} diff --git a/source/Classes/003.ManagedIdentityToken.ps1 b/source/Classes/003.ManagedIdentityToken.ps1 new file mode 100644 index 000000000..102e2e261 --- /dev/null +++ b/source/Classes/003.ManagedIdentityToken.ps1 @@ -0,0 +1,129 @@ +<# +.SYNOPSIS + Represents a managed identity token used for authentication. + +.DESCRIPTION + The ManagedIdentityToken class inherits from the AuthenticationToken class and provides functionality to handle managed identity tokens. + It includes methods to validate the token object, check if the token is expired, and retrieve the access token. + +.PARAMETER ManagedIdentityTokenObj + A PSCustomObject containing the managed identity token details. It must include the following properties: + - access_token + - expires_on + - expires_in + - resource + - token_type + +.NOTES + The class includes a constructor to initialize the token properties, a method to validate the token object, a method to check if the token is expired, and a method to retrieve the access token. + +.EXAMPLE + $tokenObj = [PSCustomObject]@{ + access_token = "your_access_token" + expires_on = 1625097600 + expires_in = 3600 + resource = "https://management.azure.com/" + token_type = "Bearer" + } + + $managedIdentityToken = New-ManagedIdentityToken -ManagedIdentityTokenObj $tokenObj + + if (-not $managedIdentityToken.isExpired()) { + $accessToken = $managedIdentityToken.Get() + Write-Output "Access Token: $accessToken" + } +#> + +class ManagedIdentityToken : AuthenticationToken +{ + + [DateTime]$expires_on + [Int]$expires_in + [String]$resource + [String]$token_type + + # Constructor + ManagedIdentityToken([PSCustomObject]$ManagedIdentityTokenObj) + { + $this.tokenType = [TokenType]::ManagedIdentity + + # Validate that ManagedIdentityTokenObj is a HashTable and Contains the correct keys + if (-not $this.isValid($ManagedIdentityTokenObj)) + { + throw "[ManagedIdentityToken] The ManagedIdentityTokenObj is not valid." + } + + $epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) + + # Set the properties of the class + $this.access_token = $ManagedIdentityTokenObj.access_token | ConvertTo-SecureString -AsPlainText -Force + $this.expires_on = $epochStart.AddSeconds($ManagedIdentityTokenObj.expires_on) + $this.expires_in = $ManagedIdentityTokenObj.expires_in + $this.resource = $ManagedIdentityTokenObj.resource + $this.token_type = $ManagedIdentityTokenObj.token_type + + } + + # Function to validate the ManagedIdentityTokenObj + Hidden [Bool]isValid($ManagedIdentityTokenObj) + { + Write-Verbose "[ManagedIdentityToken] Validating the ManagedIdentityTokenObj." + + # Assuming these are the keys we expect in the hashtable + $expectedKeys = @('access_token', 'expires_on', 'expires_in', 'resource', 'token_type') + + # Check if all expected keys exist in the hashtable + foreach ($key in $expectedKeys) + { + if (-not $ManagedIdentityTokenObj."$key") + { + Write-Verbose "[ManagedIdentityToken] The hashtable does not contain the expected property: $key" + return $false + } + } + + # If all checks pass, return true + Write-Verbose "[ManagedIdentityToken] The hashtable is valid and contains all the expected keys." + return $true + } + + [Bool]isExpired() + { + # Remove 10 seconds from the expires_on time to account for clock skew. + if ($this.expires_on.AddSeconds(-10) -lt (Get-Date)) + { + return $true + } + return $false + } + + # Return the access token + [String] Get() + { + # Verbose output + Write-Verbose "[ManagedIdentityToken] Getting the access token:" + Write-Verbose "[ManagedIdentityToken] Ensuring that the calling function is allowed to call the Get() method." + + # Test the caller + $this.TestCaller() + + Write-Verbose "[ManagedIdentityToken] Token Retrival Successful." + + # Return the access token + return ($this.ConvertFromSecureString($this.access_token)) + + } + +} + +# Function to create a new ManagedIdentityToken object +Function global:New-ManagedIdentityToken ([PSCustomObject]$ManagedIdentityTokenObj) +{ + + # Verbose output + Write-Verbose "[ManagedIdentityToken] Creating a new ManagedIdentityToken object." + + # Create and return a new ManagedIdentityToken object + return [ManagedIdentityToken]::New($ManagedIdentityTokenObj) + +} diff --git a/source/Classes/001.DscResourceBase.ps1 b/source/Classes/004.DscResourceBase.ps1 similarity index 86% rename from source/Classes/001.DscResourceBase.ps1 rename to source/Classes/004.DscResourceBase.ps1 index 9f9e9e33b..ad34d3ffe 100644 --- a/source/Classes/001.DscResourceBase.ps1 +++ b/source/Classes/004.DscResourceBase.ps1 @@ -11,7 +11,7 @@ class DscResourceBase if ([String]::IsNullOrWhiteSpace($dscResourceKeyPropertyName)) { $errorMessage = "Cannot obtain a 'DscResourceKey' value for the '$($this.GetType().Name)' instance." - New-InvalidOperationException -Message $errorMessage + throw (New-InvalidOperationException -Message $errorMessage) } return $this."$dscResourceKeyPropertyName" @@ -29,8 +29,7 @@ class DscResourceBase [System.Reflection.PropertyInfo]$propertyInfo = $_ $PropertyName = $_.Name - $propertyInfo.GetCustomAttributes($true) | - ForEach-Object { + $propertyInfo.GetCustomAttributes($true) | ForEach-Object { if ($_.TypeId.Name -eq 'DscPropertyAttribute' -and $_.Key -eq $true) @@ -43,13 +42,13 @@ class DscResourceBase if ($null -eq $dscResourceKeyPropertyNames -or $dscResourceKeyPropertyNames.Count -eq 0) { $errorMessage = "Could not obtain a 'DscResourceDscKey' property for type '$($this.GetType().Name)'." - New-InvalidOperationException -Message $errorMessage + throw (New-InvalidOperationException -Message $errorMessage) } elseif ($dscResourceKeyPropertyNames.Count -gt 1) { $errorMessage = "Obtained more than 1 property for type '$($this.GetType().Name)' that was marked as a 'Key'. There must only be 1 property on the class set as the 'Key' for DSC." - New-InvalidOperationException -Message $errorMessage + throw (New-InvalidOperationException -Message $errorMessage) } return $dscResourceKeyPropertyNames[0] @@ -67,8 +66,7 @@ class DscResourceBase $propertyInfo = $_ $PropertyName = $_.Name - $propertyInfo.GetCustomAttributes($true) | - ForEach-Object { + $propertyInfo.GetCustomAttributes($true) | ForEach-Object { if ($_.TypeId.Name -eq 'DscPropertyAttribute') { diff --git a/source/Classes/002.AzDevOpsApiDscResourceBase.ps1 b/source/Classes/005.AzDevOpsApiDscResourceBase.ps1 similarity index 96% rename from source/Classes/002.AzDevOpsApiDscResourceBase.ps1 rename to source/Classes/005.AzDevOpsApiDscResourceBase.ps1 index 191cd9721..1757977f3 100644 --- a/source/Classes/002.AzDevOpsApiDscResourceBase.ps1 +++ b/source/Classes/005.AzDevOpsApiDscResourceBase.ps1 @@ -13,8 +13,6 @@ class AzDevOpsApiDscResourceBase : DscResourceBase return $this.GetType().ToString().Replace('AzDevOps','') } - - <# .NOTES When creating an object via the Azure DevOps API, the ID (if provided) is ignored @@ -37,8 +35,6 @@ class AzDevOpsApiDscResourceBase : DscResourceBase return "$($this.ResourceName)Id" } - - <# .NOTES When creating an object via the Azure DevOps API, the 'Key' of the object will be @@ -79,7 +75,8 @@ class AzDevOpsApiDscResourceBase : DscResourceBase [RequiredAction]::Remove, [RequiredAction]::Test)) { - return "$($RequiredAction)-AzDevOps$($this.ResourceName)" + $result = "$($RequiredAction)-$($this.ResourceName)" + return $result } return $null diff --git a/source/Classes/003.AzDevOpsDscResourceBase.ps1 b/source/Classes/006.AzDevOpsDscResourceBase.ps1 similarity index 50% rename from source/Classes/003.AzDevOpsDscResourceBase.ps1 rename to source/Classes/006.AzDevOpsDscResourceBase.ps1 index ccfa36b98..665b86b09 100644 --- a/source/Classes/003.AzDevOpsDscResourceBase.ps1 +++ b/source/Classes/006.AzDevOpsDscResourceBase.ps1 @@ -4,30 +4,107 @@ #> class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase { - [DscProperty()] - [Alias('Uri')] - [System.String] - $ApiUri - - [DscProperty()] - [Alias('PersonalAccessToken')] - [System.String] - $Pat - [DscProperty()] [Ensure] $Ensure + [DscProperty(NotConfigurable)] + [Alias('result')] + [HashTable]$LookupResult + + hidden Construct() + { + Import-Module AzureDevOpsDsc.Common -ArgumentList @($true) + + # Ensure that $ENV:AZDODSC_CACHE_DIRECTORY is set. If not, throw an error. + if (-not($ENV:AZDODSC_CACHE_DIRECTORY)) + { + Write-Verbose "[AzDevOpsDscResourceBase] The Environment Variable 'AZDODSC_CACHE_DIRECTORY' is not set." + Throw "[AzDevOpsDscResourceBase] The Environment Variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the Environment Variable 'AZDODSC_CACHE_DIRECTORY' to the Cache Directory." + } + + # Attempt to import the ModuleSettings.clixml file. If it does not exist, throw an error. + $moduleSettingsPath = Join-Path -Path $ENV:AZDODSC_CACHE_DIRECTORY -ChildPath "ModuleSettings.clixml" + Write-Verbose "[AzDevOpsDscResourceBase] Looking for ModuleSettings.clixml at path: $moduleSettingsPath" + + # Check if the ModuleSettings.clixml file exists in the Cache Directory. If not, throw an error. + if (-not(Test-Path -Path $moduleSettingsPath)) + { + Throw "[AzDevOpsDscResourceBase] The ModuleSettings.clixml file does not exist in the Cache Directory. Please ensure that the file exists." + } + + Write-Verbose "[AzDevOpsDscResourceBase] Found ModuleSettings.clixml file." + + # Import the ModuleSettings.clixml file + $objectSettings = Import-Clixml -LiteralPath $moduleSettingsPath + Write-Verbose "[AzDevOpsDscResourceBase] Successfully imported ModuleSettings.clixml." + + # + # Import the Token information from the Cache Directory + + $organizationName = $objectSettings.OrganizationName + $tokenObject = $objectSettings.Token + $access_token = $tokenObject.access_token + + # Ensure that the access_token is not null or empty. If it is, throw an error. + if ([String]::IsNullOrEmpty($access_token)) + { + Throw "[AzDevOpsDscResourceBase] The Token information does not exist in the Cache Directory. Please ensure that the Token information exists." + } + + Write-Verbose "[AzDevOpsDscResourceBase] Access token retrieved successfully." + + # + # Determine the type of Token (PersonalAccessToken or ManagedIdentity) + + switch ($tokenObject.tokenType.ToString()) + { + + # If the Token is empty + { [String]::IsNullOrEmpty($_) } { + Write-Verbose "[AzDevOpsDscResourceBase] Token type is null or empty." + Throw "[AzDevOpsDscResourceBase] The Token information does not exist in the Cache Directory. Please ensure that the Token information exists." + } + # If the Token is a Personal Access Token + { $_ -eq 'PersonalAccessToken' } { + Write-Verbose "[AzDevOpsDscResourceBase] Token type is Personal Access Token." + New-AzDoAuthenticationProvider -OrganizationName $organizationName -SecureStringPersonalAccessToken $access_token -isResource -NoVerify + } + # If the Token is a Managed Identity Token + { $_ -eq 'ManagedIdentity' } { + Write-Verbose "[AzDevOpsDscResourceBase] Token type is Managed Identity." + New-AzDoAuthenticationProvider -OrganizationName $organizationName -useManagedIdentity -isResource -NoVerify + } + # Default + default { + Write-Verbose "[AzDevOpsDscResourceBase] Unknown token type." + Throw "[AzDevOpsDscResourceBase] The Token information does not exist in the Cache Directory. Please ensure that the Token information exists." + } + + } + + # + # Initialize the cache objects. Don't delete the cache objects since they are used by other resources. + + Get-AzDoCacheObjects | ForEach-Object { + Initialize-CacheObject -CacheType $_ -BypassFileCheck -Debug + Write-Verbose "[AzDevOpsDscResourceBase] Initialized cache object of type: $_" + } + + } hidden [Hashtable]GetDscCurrentStateObjectGetParameters() { # Setup a default set of parameters to pass into the resource/object's 'Get' method $getParameters = @{ - ApiUri = $this.ApiUri - Pat = $this.Pat "$($this.GetResourceKeyPropertyName())" = $this.GetResourceKey() } + # Append all other properties to the hashtable + $this.GetDscResourcePropertyNames() | ForEach-Object { + $getParameters."$_" = $this."$_" + } + # If there is an available 'ResourceId' value, add it to the parameters/hashtable if (![System.String]::IsNullOrWhiteSpace($this.GetResourceId())) { @@ -37,7 +114,6 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase return $getParameters } - hidden [PsObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) { # Obtain the 'Get' function name for the object, then invoke it @@ -45,21 +121,27 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase return $(& $thisResourceGetFunctionName @GetParameters) } - - hidden [System.Management.Automation.PSObject]GetDscCurrentStateObject() + hidden [HashTable]GetDscCurrentStateObject() { - $getParameters = $this.GetDscCurrentStateObjectGetParameters() - $dscCurrentStateResourceObject = $this.GetDscCurrentStateResourceObject($getParameters) + # Declare the result hashtable + $props = @{} - # If no object was returned (i.e it does not exist), create a default/empty object - if ($null -eq $dscCurrentStateResourceObject) - { - return New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{ - Ensure = [Ensure]::Absent - } + $getParameters = $this.GetDscCurrentStateObjectGetParameters() + + # Add all properties from the current object to the hashtable + $getParameters.Keys | ForEach-Object { + $props."$_" = $this."$_" } - return $dscCurrentStateResourceObject + $props.LookupResult = $this.GetDscCurrentStateResourceObject($getParameters) + $props.Ensure = $( + if ($null -eq $props.LookupResult.Ensure) { [Ensure]::Absent } + else { $props.LookupResult.Ensure } + ) + $props.LookupResult.Ensure + + return $props + } @@ -82,6 +164,23 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase return $null } + hidden [Object[]]FindEnumValuesForInteger([System.Type]$EnumType, [Int32]$Value) + { + [System.Collections.ArrayList]$enumValues = @() + + [System.Array]$enumValues = [System.Enum]::GetValues($EnumType) + + [System.Collections.ArrayList]$matchingEnumValues = @() + + $enumValues | ForEach-Object { + if ($Value -band $_ -eq $_) + { + $matchingEnumValues.Add($_) + } + } + + return $matchingEnumValues.ToArray() + } hidden [Hashtable]GetDscDesiredStateProperties() { @@ -98,109 +197,79 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase hidden [RequiredAction]GetDscRequiredAction() { + # Initialize required action as None + $dscRequiredAction = [RequiredAction]::None + + # Retrieve current and desired properties [Hashtable]$currentProperties = $this.GetDscCurrentStateProperties() [Hashtable]$desiredProperties = $this.GetDscDesiredStateProperties() + # Retrieve property names [System.String[]]$dscPropertyNamesWithNoSetSupport = $this.GetDscResourcePropertyNamesWithNoSetSupport() [System.String[]]$dscPropertyNamesToCompare = $this.GetDscResourcePropertyNames() - # Update 'Id' property: - # Set $desiredProperties."$IdPropertyName" to $currentProperties."$IdPropertyName" if it's desired - # value is blank/null but it's current/existing value is known (and can be recovered from $currentProperties). - # - # This ensures that alternate keys (typically ResourceIds) not provided in the DSC configuration do not flag differences - [System.String]$IdPropertyName = $this.GetResourceIdPropertyName() - - if ([System.String]::IsNullOrWhiteSpace($desiredProperties[$IdPropertyName]) -and - ![System.String]::IsNullOrWhiteSpace($currentProperties[$IdPropertyName])) - { - $desiredProperties."$IdPropertyName" = $currentProperties."$IdPropertyName" - } - - - # Perform logic with 'Ensure' (to determine whether resource should be created or dropped (or updated, if already [Ensure]::Present but property values differ) - $dscRequiredAction = [RequiredAction]::None - switch ($desiredProperties.Ensure) { ([Ensure]::Present) { - # If not already present, or different to expected/desired - return [RequiredAction]::New (i.e. Resource needs creating) - if ($null -eq $currentProperties -or $($currentProperties.Ensure) -ne [Ensure]::Present) - { - $dscRequiredAction = [RequiredAction]::New - Write-Verbose "DscActionRequired='$dscRequiredAction'" - break - } + Write-Verbose "Desired state is Present." - # Changes made by DSC to the following properties are unsupported by the resource (other than when creating a [RequiredAction]::New resource) - if ($dscPropertyNamesWithNoSetSupport.Count -gt 0) + switch ($currentProperties.LookupResult.Status) { - $dscPropertyNamesWithNoSetSupport | ForEach-Object { - - if ($($currentProperties[$_].ToString()) -ne $($desiredProperties[$_].ToString())) - { - $errorMessage = "The '$($this.GetType().Name)', DSC Resource does not support changes for/to the '$_' property." - New-InvalidOperationException -Message $errorMessage - } - } - } - - # Compare all properties ('Current' vs 'Desired') - if ($dscPropertyNamesToCompare.Count -gt 0) - { - $dscPropertyNamesToCompare | ForEach-Object { - - if ($($currentProperties."$_") -ne $($desiredProperties."$_")) - { - Write-Verbose "DscPropertyValueMismatch='$_'" - $dscRequiredAction = [RequiredAction]::Set - } + ([DSCGetSummaryState]::NotFound) { + $dscRequiredAction = [RequiredAction]::New + Write-Verbose "Resource not found. Setting action to New." + }([DSCGetSummaryState]::Changed) { + $dscRequiredAction = [RequiredAction]::Set + Write-Verbose "Resource Changed. Setting action to Set." + }([DSCGetSummaryState]::Renamed) { + $dscRequiredAction = [RequiredAction]::Set + Write-Verbose "Resource Renamed. Setting action to Set." + }([DSCGetSummaryState]::Missing) { + $dscRequiredAction = [RequiredAction]::Remove + Write-Verbose "Resource missing. Setting action to Remove." + }([DSCGetSummaryState]::Unchanged) { + $dscRequiredAction = [RequiredAction]::None + Write-Verbose "Resource Not Changed. Setting action to Remove." } - - if ($dscRequiredAction -eq [RequiredAction]::Set) - { - Write-Verbose "DscActionRequired='$dscRequiredAction'" - break + default { + $errorMessage = "Could not obtain a valid 'LookupResult.Status' value within '$($this.GetResourceName())' Test() function. Value was '$($currentProperties.LookupResult.Status)'" + Write-Verbose $errorMessage + throw (New-InvalidOperationException -Message $errorMessage) } } - # Otherwise, no changes to make (i.e. The desired state is already achieved) + Write-Verbose "DscActionRequired='$dscRequiredAction'" return $dscRequiredAction - break + } + ([Ensure]::Absent) { - # If currently/already present - return $false (i.e. state is incorrect) - if ($null -ne $currentProperties -and $currentProperties.Ensure -ne [Ensure]::Absent) - { - $dscRequiredAction = [RequiredAction]::Remove - Write-Verbose "DscActionRequired='$dscRequiredAction'" - break - } + Write-Verbose "Desired state is Absent." + + $dscRequiredAction = ($currentProperties.LookupResult.Status -eq [DSCGetSummaryState]::NotFound) ? [RequiredAction]::None : [RequiredAction]::Remove - # Otherwise, no changes to make (i.e. The desired state is already achieved) Write-Verbose "DscActionRequired='$dscRequiredAction'" return $dscRequiredAction - break } + default { $errorMessage = "Could not obtain a valid 'Ensure' value within '$($this.GetResourceName())' Test() function. Value was '$($desiredProperties.Ensure)'." - New-InvalidOperationException -Message $errorMessage + Write-Verbose $errorMessage + throw (New-InvalidOperationException -Message $errorMessage) } } return $dscRequiredAction } - hidden [Hashtable]GetDesiredStateParameters([Hashtable]$CurrentStateProperties, [Hashtable]$DesiredStateProperties, [RequiredAction]$RequiredAction) { [Hashtable]$desiredStateParameters = $DesiredStateProperties [System.String]$IdPropertyName = $this.GetResourceIdPropertyName() - # If actions required are 'None' or 'Error', return a $null value if ($RequiredAction -in @([RequiredAction]::None, [RequiredAction]::Error)) { @@ -209,19 +278,14 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase # If the desired state/action is to remove the resource, generate/return a minimal set of parameters required to remove the resource elseif ($RequiredAction -eq [RequiredAction]::Remove) { - return @{ - ApiUri = $DesiredStateProperties.ApiUri - Pat = $DesiredStateProperties.Pat - Force = $true + return $desiredStateParameters - # Set this from the 'Current' state as we would expect this to have an existing key/ID value to use - "$IdPropertyName" = $CurrentStateProperties."$IdPropertyName" - } } # If the desired state/action is to add/new or update/set the resource, start with the values in the $DesiredStateProperties variable, and amend elseif ($RequiredAction -in @([RequiredAction]::New, [RequiredAction]::Set)) { + # Set $desiredParameters."$IdPropertyName" to $CurrentStateProperties."$IdPropertyName" if it's known and can be recovered from existing resource if ([System.String]::IsNullOrWhiteSpace($desiredStateParameters."$IdPropertyName") -and ![System.String]::IsNullOrWhiteSpace($CurrentStateProperties."$IdPropertyName")) @@ -234,14 +298,12 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase $desiredStateParameters.Remove($IdPropertyName) } - # Do not need/want this passing as a parameter (the action taken will determine the desired state) $desiredStateParameters.Remove('Ensure') # Add this to 'Force' subsequent function call $desiredStateParameters.Force = $true - # Some DSC properties are only supported for 'New' and 'Remove' actions, but not 'Set' ones (these need to be removed) [System.String[]]$unsupportedForSetPropertyNames = $this.GetDscResourcePropertyNamesWithNoSetSupport() @@ -292,6 +354,7 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase [void] SetToDesiredState() { [RequiredAction]$dscRequiredAction = $this.GetDscRequiredAction() + $cacheProperties = $false if ($dscRequiredAction -in @([RequiredAction]::'New', [RequiredAction]::'Set', [RequiredAction]::'Remove')) { @@ -301,6 +364,12 @@ class AzDevOpsDscResourceBase : AzDevOpsApiDscResourceBase $dscRequiredActionFunctionName = $this.GetResourceFunctionName($dscRequiredAction) $dscDesiredStateParameters = $this.GetDesiredStateParameters($dscCurrentStateProperties, $dscDesiredStateProperties, $dscRequiredAction) + # Set the lookup properties on the desired state object. Since it will be used to set the resource. + if ($null -ne $dscCurrentStateProperties.LookupResult) + { + $dscDesiredStateParameters.LookupResult = $dscCurrentStateProperties.LookupResult + } + & $dscRequiredActionFunctionName @dscDesiredStateParameters | Out-Null Start-Sleep -Milliseconds $($this.GetPostSetWaitTimeMs()) } diff --git a/source/Classes/007.APIRateLimit.ps1 b/source/Classes/007.APIRateLimit.ps1 new file mode 100644 index 000000000..94480ebc9 --- /dev/null +++ b/source/Classes/007.APIRateLimit.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + Represents the API rate limit information. + +.DESCRIPTION + The APIRateLimit class encapsulates the details of API rate limiting, including retry-after duration, + remaining rate limit, and rate limit reset time. It provides constructors to initialize these properties + and a method to validate the input hashtable. + +.PROPERTIES + [Int]$retryAfter + The duration (in seconds) to wait before retrying the API request. + + [Int]$xRateLimitRemaining + The number of remaining API requests allowed in the current rate limit window. + + [Int]$xRateLimitReset + The time (in Unix epoch format) when the rate limit will reset. + +.METHODS + [void] APIRateLimit([HashTable]$APIRateLimitObj) + Constructor that initializes the APIRateLimit object using a hashtable containing the rate limit details. + Throws an error if the hashtable is not valid. + + [void] APIRateLimit([int]$retryAfter) + Constructor that initializes the APIRateLimit object with a specified retry-after duration. + + [Bool] isValid([HashTable]$APIRateLimitObj) + Validates that the provided hashtable contains the expected keys for rate limit information. + Returns $true if the hashtable is valid, otherwise returns $false. +#> + +class APIRateLimit +{ + + [Int]$retryAfter = 0 + [Int]$xRateLimitRemaining = 0 + [Int]$xRateLimitReset = 0 + + # Constructor + APIRateLimit([HashTable]$APIRateLimitObj) + { + + # Validate that APIRateLimitObj is a HashTable and Contains the correct keys + if (-not $this.isValid($APIRateLimitObj)) + { + throw "The APIRateLimitObj is not valid." + } + + # Convert X-RateLimit-Reset from Unix Time to DateTime + $epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) + + # Set the properties of the class + $this.retryAfter = [Int]($APIRateLimitObj.'Retry-After') + $this.XRateLimitRemaining = [Int]$APIRateLimitObj.'X-RateLimit-Remaining' + $this.XRateLimitReset = [Int]($APIRateLimitObj.'X-RateLimit-Reset') + + } + + # Constructor with retryAfter Parameters + APIRateLimit($retryAfter) + { + + # Set the properties of the class + $this.retryAfter = [int]$retryAfter + + } + + Hidden [Bool]isValid($APIRateLimitObj) + { + + # Assuming these are the keys we expect in the hashtable + $expectedKeys = @('Retry-After', 'X-RateLimit-Remaining', 'X-RateLimit-Reset') + + # Check if all expected keys exist in the hashtable + foreach ($key in $expectedKeys) + { + if (-not $APIRateLimitObj.ContainsKey($key)) + { + Write-Warning "[APIRateLimit] The hashtable does not contain the expected key: $key" + return $false + } + } + + # If all checks pass, return true + Write-Verbose "[APIRateLimit] The hashtable is valid and contains all the expected keys." + return $true + + } + +} diff --git a/source/Classes/009.AzDoGroupPermission.ps1 b/source/Classes/009.AzDoGroupPermission.ps1 new file mode 100644 index 000000000..6693563a3 --- /dev/null +++ b/source/Classes/009.AzDoGroupPermission.ps1 @@ -0,0 +1,112 @@ +<# +.SYNOPSIS + This class represents a DSC resource for managing Azure DevOps project group permissions. + +.DESCRIPTION + The AzDoGroupPermission class is a DSC resource that allows you to manage permissions for a group in an Azure DevOps project. + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 + +.LINK + GitHub Repository: + +.PARAMETER GroupName + The name of the group for which the permissions are being managed. + +.PARAMETER ProjectName + The name of the Azure DevOps project. + +.PARAMETER isInherited + Specifies whether the permissions are inherited from a parent group. Default value is $true. + +.PARAMETER Permissions + Specifies the permissions to be assigned to the group. This should be an array of hashtables, where each hashtable represents a permission. + +.EXAMPLE + This example shows how to use the AzDoGroupPermission resource to manage permissions for a group in an Azure DevOps project. + + Configuration Example { + Import-DscResource -ModuleName AzDevOpsDsc + + Node localhost { + AzDoGroupPermission GroupPermission { + GroupName = 'MyGroup' + ProjectName = 'MyProject' + Permissions = @( + @{ + Permission = 'Read' + Allow = $true + }, + @{ + Permission = 'Write' + Allow = $false + } + ) + } + } + } + +#> + +# RESOURCE IS CURRENTLY DISABLED +#[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoGroupPermission : AzDevOpsDscResourceBase +{ + [DscProperty(Key, Mandatory)] + [Alias('Name')] + [System.String]$GroupName + + [DscProperty()] + [Alias('Inherited')] + [System.Boolean]$isInherited=$true + + [DscProperty()] + [HashTable[]]$Permissions + + AzDoGroupPermission() + { + $this.Construct() + } + + AzDoGroupPermission([bool]$isTest) + { + } + + [AzDoGroupPermission] Get() + { + return [AzDoGroupPermission]$($this.GetDscCurrentStateProperties()) + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @() + } + + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.GroupName = $CurrentResourceObject.GroupName + $properties.isInherited = $CurrentResourceObject.isInherited + $properties.Permissions = $CurrentResourceObject.Permissions + $properties.lookupResult = $CurrentResourceObject.lookupResult + $properties.Ensure = $CurrentResourceObject.Ensure + + Write-Verbose "[AzDoGroupPermission] Current state properties: $($properties | Out-String)" + + return $properties + } + +} diff --git a/source/Classes/010.AzDevOpsProject.ps1 b/source/Classes/010.AzDevOpsProject.ps1 deleted file mode 100644 index 4dea38bbc..000000000 --- a/source/Classes/010.AzDevOpsProject.ps1 +++ /dev/null @@ -1,78 +0,0 @@ -<# - .SYNOPSIS - A DSC Resource for Azure DevOps that represents the 'Project' resource. - - .DESCRIPTION - A DSC Resource for Azure DevOps that represents the 'Project' resource. - - .PARAMETER ProjectId - The 'Id' of the Azure DevOps, 'Project' resource. - - .PARAMETER ProjectName - The 'Name' of the Azure DevOps, 'Project' resource. - - .PARAMETER ProjectDescription - The 'Description' of the Azure DevOps, 'Project' resource. - - .PARAMETER SourceControlType - The 'SourceControlType' of the Azure DevOps, 'Project' resource. - - If the 'Project' resource already exists in Azure DevOps, the parameter - `SourceControlType` cannot be used to change to another type. -#> -[DscResource()] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] -class AzDevOpsProject : AzDevOpsDscResourceBase -{ - [DscProperty()] - [Alias('Id')] - [System.String]$ProjectId - - [DscProperty(Key, Mandatory)] - [Alias('Name')] - [System.String]$ProjectName - - [DscProperty()] - [Alias('Description')] - [System.String]$ProjectDescription - - [DscProperty()] - [ValidateSet('Git', 'Tfvc')] - [System.String]$SourceControlType - - - [AzDevOpsProject] Get() - { - return [AzDevOpsProject]$($this.GetDscCurrentStateProperties()) - } - - - hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() - { - return @('SourceControlType') - } - - hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) - { - $properties = @{ - Pat = $this.Pat - ApiUri = $this.ApiUri - Ensure = [Ensure]::Absent - } - - if ($null -ne $CurrentResourceObject) - { - if (![System.String]::IsNullOrWhiteSpace($CurrentResourceObject.id)) - { - $properties.Ensure = [Ensure]::Present - } - $properties.ProjectId = $CurrentResourceObject.id - $properties.ProjectName = $CurrentResourceObject.name - $properties.ProjectDescription = $CurrentResourceObject.description - $properties.SourceControlType = $CurrentResourceObject.capabilities.versioncontrol.sourceControlType - } - - return $properties - } - -} diff --git a/source/Classes/011.AzDoOrganizationGroup.ps1 b/source/Classes/011.AzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..f8f4e3018 --- /dev/null +++ b/source/Classes/011.AzDoOrganizationGroup.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS + This class represents an Azure DevOps organization group. + +.DESCRIPTION + The AzDoOrganizationGroup class is a DSC resource that allows you to manage Azure DevOps organization groups. + It provides properties to specify the group name, display name, and description. + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 + +.LINK + GitHub Repository: + +.PARAMETER GroupName + The name of the organization group. + This property is mandatory and serves as the key property for the resource. + +.PARAMETER GroupDescription + The description of the organization group. + +.INPUTS + None. + +.OUTPUTS + None. + +.EXAMPLE + This example shows how to create an instance of the AzDoOrganizationGroup class: + + $organizationGroup = [AzDoOrganizationGroup]::new() + $organizationGroup.GroupName = "MyGroup" + $organizationGroup.GroupDescription = "This is my group." + + $organizationGroup.Get() + +#> + +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoOrganizationGroup : AzDevOpsDscResourceBase +{ + [DscProperty(Key, Mandatory)] + [Alias('Name')] + [System.String]$GroupName + + [DscProperty()] + [Alias('Description')] + [System.String]$GroupDescription + + AzDoOrganizationGroup() + { + $this.Construct() + } + + [AzDoOrganizationGroup] Get() + { + return [AzDoOrganizationGroup]$($this.GetDscCurrentStateProperties()) + } + + hidden [HashTable] getDscCurrentAPIState() + { + # Get the current state of the resource + $params = @{ + GroupName = $this.GroupName + GroupDescription = $this.GroupDescription + } + + return Get-AzDoOrganizationGroup @params + + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @() + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.GroupName = $CurrentResourceObject.GroupName + $properties.GroupDescription = $CurrentResourceObject.GroupDescription + $properties.Ensure = $CurrentResourceObject.Ensure + $properties.LookupResult = $CurrentResourceObject.LookupResult + #$properties.Reasons = $CurrentResourceObject.LookupResult.Reasons + + Write-Verbose "[AzDoOrganizationGroup] Current state properties: $($properties | Out-String)" + + return $properties + } +} diff --git a/source/Classes/020.AzDoProject.ps1 b/source/Classes/020.AzDoProject.ps1 new file mode 100644 index 000000000..0e10dc775 --- /dev/null +++ b/source/Classes/020.AzDoProject.ps1 @@ -0,0 +1,113 @@ +<# +.SYNOPSIS + This class represents an Azure DevOps project. + +.DESCRIPTION + The AzDoProject class is used to define and manage Azure DevOps projects. It inherits from the AzDevOpsDscResourceBase class. + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 + +.LINK + GitHub Repository: + +.PARAMETER ProjectName + The name of the Azure DevOps project. + +.PARAMETER ProjectDescription + The description of the Azure DevOps project. + +.PARAMETER SourceControlType + The type of source control for the project. Valid values are 'Git' and 'Tfvc'. + +.PARAMETER ProcessTemplate + The process template for the project. Valid values are 'Agile', 'Scrum', 'CMMI', and 'Basic'. + +.PARAMETER Visibility + The visibility of the project. Valid values are 'Public' and 'Private'. + +.INPUTS + None + +.OUTPUTS + None + +.EXAMPLE + $project = [AzDoProject]::Get() + $project.ProjectName = 'MyProject' + $project.ProjectDescription = 'This is a sample project' + $project.SourceControlType = 'Git' + $project.ProcessTemplate = 'Agile' + $project.Visibility = 'Private' + $project | Set-AzDevOpsProject + +#> + +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoProject : AzDevOpsDscResourceBase +{ + [DscProperty(Key, Mandatory)] + [Alias('Name')] + [System.String]$ProjectName + + [DscProperty()] + [Alias('Description')] + [System.String]$ProjectDescription + + [DscProperty()] + [ValidateSet('Git', 'Tfvc')] + [System.String]$SourceControlType = 'Git' + + [DscProperty()] + [ValidateSet('Agile', 'Scrum', 'CMMI', 'Basic')] + [System.String]$ProcessTemplate = 'Agile' + + [DscProperty()] + [ValidateSet('Public', 'Private')] + [System.String]$Visibility = 'Private' + + AzDoProject() + { + $this.Construct() + } + + [AzDoProject] Get() + { + return [AzDoProject]$($this.GetDscCurrentStateProperties()) + } + + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @('SourceControlType','ProcessTemplate') + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.ProjectName = $CurrentResourceObject.ProjectName + $properties.ProjectDescription = $CurrentResourceObject.ProjectDescription + $properties.SourceControlType = $CurrentResourceObject.SourceControlType + $properties.ProcessTemplate = $CurrentResourceObject.ProcessTemplate + $properties.Visibility = $CurrentResourceObject.Visibility + $properties.LookupResult = $CurrentResourceObject.LookupResult + $properties.Ensure = $CurrentResourceObject.Ensure + + Write-Verbose "[AzDoGroupPermission] Current state properties: $($properties | Out-String)" + + return $properties + + } + +} diff --git a/source/Classes/021.AzDoProjectServices.ps1 b/source/Classes/021.AzDoProjectServices.ps1 new file mode 100644 index 000000000..7998e87e9 --- /dev/null +++ b/source/Classes/021.AzDoProjectServices.ps1 @@ -0,0 +1,125 @@ +<# +.SYNOPSIS + This class represents an Azure DevOps project and its associated services. + +.DESCRIPTION + The AzDoProjectServices class is a DSC resource that allows you to manage the services associated with an Azure DevOps project. It provides properties to enable or disable various services such as Git repositories, work boards, build pipelines, test plans, and Azure artifacts. + +.PARAMETER ProjectName + The name of the Azure DevOps project. + +.PARAMETER GitRepositories + Specifies whether Git repositories are enabled or disabled for the project. Valid values are 'Enabled' and 'Disabled'. The default value is 'Enabled'. + +.PARAMETER WorkBoards + Specifies whether work boards are enabled or disabled for the project. Valid values are 'Enabled' and 'Disabled'. The default value is 'Enabled'. + +.PARAMETER BuildPipelines + Specifies whether build pipelines are enabled or disabled for the project. Valid values are 'Enabled' and 'Disabled'. The default value is 'Enabled'. + +.PARAMETER TestPlans + Specifies whether test plans are enabled or disabled for the project. Valid values are 'Enabled' and 'Disabled'. The default value is 'Enabled'. + +.PARAMETER AzureArtifact + Specifies whether Azure artifacts are enabled or disabled for the project. Valid values are 'Enabled' and 'Disabled'. The default value is 'Enabled'. + +.EXAMPLE + This example shows how to use the AzDoProjectServices resource to manage the services of an Azure DevOps project. + + Configuration Example { + Import-DscResource -ModuleName xAzDevOpsDSC + + Node localhost { + AzDoProjectServices ProjectServices { + ProjectName = 'MyProject' + GitRepositories = 'Enabled' + WorkBoards = 'Disabled' + BuildPipelines = 'Enabled' + TestPlans = 'Disabled' + AzureArtifact = 'Enabled' + Ensure = 'Present' + } + } + } + +.NOTES + Version: 1.0 + Author: Michael Zanatta + Required Modules: xAzDevOpsDSC +#> + +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoProjectServices : AzDevOpsDscResourceBase +{ + [DscProperty(Mandatory, Key)] + [Alias('Name')] + [System.String]$ProjectName + + [DscProperty()] + [Alias('Repos')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$GitRepositories = 'Enabled' + + [DscProperty()] + [Alias('Board')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$WorkBoards = 'Enabled' + + [DscProperty()] + [Alias('Pipelines')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$BuildPipelines = 'Enabled' + + [DscProperty()] + [Alias('Tests')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$TestPlans = 'Enabled' + + [DscProperty()] + [Alias('Artifacts')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$AzureArtifact = 'Enabled' + + AzDoProjectServices() + { + $this.Construct() + } + + [AzDoProjectServices] Get() + { + return [AzDoProjectServices]$($this.GetDscCurrentStateProperties()) + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @() + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.ProjectName = $CurrentResourceObject.ProjectName + $properties.GitRepositories = $CurrentResourceObject.GitRepositories + $properties.WorkBoards = $CurrentResourceObject.WorkBoards + $properties.BuildPipelines = $CurrentResourceObject.BuildPipelines + $properties.TestPlans = $CurrentResourceObject.TestPlans + $properties.AzureArtifact = $CurrentResourceObject.AzureArtifact + $properties.Ensure = $CurrentResourceObject.Ensure + $properties.LookupResult = $CurrentResourceObject.LookupResult + + Write-Verbose "[AzDoProjectGroup] Current state properties: $($properties | Out-String)" + + return $properties + } + +} diff --git a/source/Classes/022.AzDoProjectGroup.ps1 b/source/Classes/022.AzDoProjectGroup.ps1 new file mode 100644 index 000000000..9fb02a07a --- /dev/null +++ b/source/Classes/022.AzDoProjectGroup.ps1 @@ -0,0 +1,112 @@ +<# +.SYNOPSIS + This class represents an Azure DevOps project group. + +.DESCRIPTION + The AzDoProjectGroup class is a DSC resource that allows you to manage Azure DevOps project groups. + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 + +.LINK + GitHub Repository: + +.PARAMETER ProjectName + The name of the Azure DevOps project associated with the project group. + +.PARAMETER GroupName + The name of the project group. + +.PARAMETER GroupDescription + The description of the project group. + +.EXAMPLE + This example shows how to create a new project group: + + $projectGroup = [AzDoProjectGroup]::new() + $projectGroup.ProjectName = "MyProject" + $projectGroup.GroupName = "MyGroup" + $projectGroup.GroupDescription = "This is my project group." + $projectGroup.Ensure = "Present" + $projectGroup.Pat = "**********" + $projectGroup.ApiUri = "https://dev.azure.com/MyOrganization" + + $projectGroup | Set-DscResource + +.INPUTS + None + +.OUTPUTS + None +#> + +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoProjectGroup : AzDevOpsDscResourceBase +{ + [DscProperty(Key, Mandatory)] + [Alias('Name')] + [System.String]$GroupName + + [DscProperty(Mandatory)] + [Alias('Project')] + [System.String]$ProjectName + + [DscProperty()] + [Alias('Description')] + [System.String]$GroupDescription + + AzDoProjectGroup() + { + $this.Construct() + } + + [AzDoProjectGroup] Get() + { + return [AzDoProjectGroup]$($this.GetDscCurrentStateProperties()) + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @() + } + + hidden [HashTable] getDscCurrentAPIState() + { + # Get the current state of the resource + $params = @{ + GroupName = $this.GroupName + GroupDescription = $this.GroupDescription + ProjectName = $this.ProjectName + } + + return Get-AzDoProjectGroup @params + + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.GroupName = $CurrentResourceObject.GroupName + $properties.GroupDescription = $CurrentResourceObject.GroupDescription + $properties.ProjectName = $CurrentResourceObject.ProjectName + $properties.Ensure = $CurrentResourceObject.Ensure + $properties.LookupResult = $CurrentResourceObject.LookupResult + #$properties.Reasons = $CurrentResourceObject.LookupResult.Reasons + + Write-Verbose "[AzDoProjectGroup] Current state properties: $($properties | Out-String)" + + return $properties + } + +} diff --git a/source/Classes/031.AzDoGroupMember.ps1 b/source/Classes/031.AzDoGroupMember.ps1 new file mode 100644 index 000000000..69c115d41 --- /dev/null +++ b/source/Classes/031.AzDoGroupMember.ps1 @@ -0,0 +1,94 @@ +<# +.SYNOPSIS + This class represents a DSC resource for managing Azure DevOps group members. + +.DESCRIPTION + The AzDoGroupMember class is a DSC resource that allows you to manage the members of an Azure DevOps group. + It inherits from the AzDevOpsDscResourceBase class. + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 + +.LINK + GitHub Repository: + +.PARAMETER GroupName + The name of the Azure DevOps group. + +.PARAMETER GroupMembers + An array of strings representing the members of the Azure DevOps group. + +.EXAMPLE + This example shows how to use the AzDoGroupMember resource to add members to an Azure DevOps group. + + Configuration Example { + Import-DscResource -ModuleName AzDoGroupMember + + Node localhost { + AzDoGroupMember GroupMember { + GroupName = 'MyGroup' + GroupMembers = @('User1', 'User2', 'User3') + Ensure = 'Present' + } + } + } + +.INPUTS + None + +.OUTPUTS + None + +#> + +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoGroupMember : AzDevOpsDscResourceBase +{ + [DscProperty(Key, Mandatory)] + [Alias('Name')] + [System.String]$GroupName + + [DscProperty(Mandatory)] + [Alias('Members')] + [System.String[]]$GroupMembers + + AzDoGroupMember() + { + $this.Construct() + } + + [AzDoGroupMember] Get() + { + return [AzDoGroupMember]$($this.GetDscCurrentStateProperties()) + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @() + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.GroupName = $CurrentResourceObject.GroupName + $properties.GroupMembers = $CurrentResourceObject.GroupMembers + $properties.Ensure = $CurrentResourceObject.Ensure + $properties.LookupResult = $CurrentResourceObject.LookupResult + + Write-Verbose "[AzDoProjectGroup] Current state properties: $($properties | Out-String)" + + return $properties + } + +} diff --git a/source/Classes/040.AzDoGitRepository.ps1 b/source/Classes/040.AzDoGitRepository.ps1 new file mode 100644 index 000000000..dcd627879 --- /dev/null +++ b/source/Classes/040.AzDoGitRepository.ps1 @@ -0,0 +1,100 @@ +<# +.SYNOPSIS + This class represents an Azure DevOps Git repository. + +.DESCRIPTION + The AzDoGitRepository class is a DSC resource that allows you to manage Azure DevOps Git repositories. + It inherits from the AzDevOpsDscResourceBase class. + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 + +.LINK + GitHub Repository: + +.PARAMETER ProjectName + The name of the Azure DevOps project where the Git repository is located. + +.PARAMETER GitRepositoryName + The name of the Git repository. + +.PARAMETER SourceRepository + The source repository URL. + +.EXAMPLE + This example shows how to use the AzDoGitRepository resource to ensure that a Git repository exists in an Azure DevOps project. + + Configuration Example { + Import-DscResource -ModuleName AzDoGitRepository + + AzDoGitRepository MyGitRepository { + ProjectName = 'MyProject' + GitRepositoryName = 'MyRepository' + SourceRepository = 'https://github.com/MyUser/MyRepository.git' + Ensure = 'Present' + } + } + +.INPUTS + None + +.OUTPUTS + None +#> + +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoGitRepository : AzDevOpsDscResourceBase +{ + [DscProperty(Mandatory)] + [Alias('Name')] + [System.String]$ProjectName + + [DscProperty(Key, Mandatory)] + [Alias('Repository')] + [System.String]$RepositoryName + + [DscProperty()] + [Alias('Source')] + [System.String]$SourceRepository + + AzDoGitRepository() + { + $this.Construct() + } + + [AzDoGitRepository] Get() + { + return [AzDoGitRepository]$($this.GetDscCurrentStateProperties()) + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @('ProjectName', 'RepositoryName', 'SourceRepository') + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.ProjectName = $CurrentResourceObject.ProjectName + $properties.RepositoryName = $CurrentResourceObject.RepositoryName + $properties.SourceRepository = $CurrentResourceObject.SourceRepository + $properties.Ensure = $CurrentResourceObject.Ensure + $properties.LookupResult = $CurrentResourceObject.LookupResult + + Write-Verbose "[AzDoProjectGroup] Current state properties: $($properties | Out-String)" + + return $properties + } + +} diff --git a/source/Classes/041.AzDoGitPermission.ps1 b/source/Classes/041.AzDoGitPermission.ps1 new file mode 100644 index 000000000..c34f1169e --- /dev/null +++ b/source/Classes/041.AzDoGitPermission.ps1 @@ -0,0 +1,102 @@ +<# +.SYNOPSIS + This class represents an Azure DevOps DSC resource for managing Git permissions. + +.DESCRIPTION + The AzDoGitPermission class is a DSC resource that allows you to manage Git permissions in Azure DevOps. It inherits from the AzDevOpsDscResourceBase class and provides properties and methods for managing Git permissions. + +.PARAMETER ProjectName + Specifies the name of the Azure DevOps project. + +.PARAMETER RepositoryName + Specifies the name of the Git repository. + +.PARAMETER isInherited + Specifies whether the permissions are inherited from the parent repository. Default value is $true. + +.PARAMETER PermissionsList + Specifies the list of permissions to be set for the repository. + +.NOTES + This class is part of the AzureDevOpsDSC module. + +.LINK + https://github.com/dsccommunity/AzureDevOpsDsc + +.EXAMPLE + This example shows how to use the AzDoGitPermission class to manage Git permissions in Azure DevOps. + + Configuration Example { + Import-DscResource -ModuleName AzureDevOpsDSC + + Node localhost { + AzDoGitPermission GitPermission { + ProjectName = 'MyProject' + RepositoryName = 'MyRepository' + PermissionsList = @('Read', 'Contribute') + Ensure = 'Present' + } + } + } + +#> + +[DscResource()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCStandardDSCFunctionsInResource', '', Justification='Test() and Set() method are inherited from base, "AzDevOpsDscResourceBase" class')] +class AzDoGitPermission : AzDevOpsDscResourceBase +{ + [DscProperty(Key, Mandatory)] + [Alias('Name')] + [System.String]$ProjectName + + [DscProperty(Mandatory)] + [Alias('Repository')] + [System.String]$RepositoryName + + [DscProperty()] + [Alias('Inherited')] + [System.Boolean]$isInherited=$true + + [DscProperty()] + [HashTable[]]$Permissions + + AzDoGitPermission() + { + $this.Construct() + } + + [AzDoGitPermission] Get() + { + return [AzDoGitPermission]$($this.GetDscCurrentStateProperties()) + } + + hidden [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() + { + return @() + } + + hidden [Hashtable]GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) + { + $properties = @{ + Ensure = [Ensure]::Absent + } + + # If the resource object is null, return the properties + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.ProjectName = $CurrentResourceObject.ProjectName + $properties.RepositoryName = $CurrentResourceObject.RepositoryName + $properties.isInherited = $CurrentResourceObject.isInherited + $properties.Permissions = $CurrentResourceObject.Permissions + $properties.lookupResult = $CurrentResourceObject.lookupResult + $properties.Ensure = $CurrentResourceObject.Ensure + + Write-Verbose "[AzDoGitPermission] Current state properties: $($properties | Out-String)" + + return $properties + } + +} diff --git a/source/Enum/DSCGetSummaryState.ps1 b/source/Enum/DSCGetSummaryState.ps1 new file mode 100644 index 000000000..1c7e731a0 --- /dev/null +++ b/source/Enum/DSCGetSummaryState.ps1 @@ -0,0 +1,32 @@ +<# +.SYNOPSIS + Enumeration for DSC Get Summary State. + +.DESCRIPTION + This enumeration defines the possible states for DSC (Desired State Configuration) summary results. + +.MEMBERS + Changed + Indicates that the state has changed. + Unchanged + Indicates that the state is unchanged. + NotFound + Indicates that the state was not found. + Renamed + Indicates that the state has been renamed. + Missing + Indicates that the state is missing. +#> +enum DSCGetSummaryState +{ + # Changed + Changed = 0 + # Unchanged + Unchanged = 1 + # Not Found + NotFound = 2 + # Renamed + Renamed = 3 + # Missing + Missing = 4 +} diff --git a/source/Enum/DescriptorType.ps1 b/source/Enum/DescriptorType.ps1 new file mode 100644 index 000000000..fda06bd38 --- /dev/null +++ b/source/Enum/DescriptorType.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS +Defines an enumeration for various descriptor types used in Azure DevOps. + +.DESCRIPTION +The `DescriptorType` enumeration lists various descriptor types that are used within Azure DevOps. These descriptors represent different services, features, and components within the Azure DevOps environment. + +.ENUMERATION MEMBERS +- Analytics +- AnalyticsViews +- ReleaseManagement +- AuditLog +- Identity +- WorkItemTrackingAdministration +- DistributedTask +- GitRepositories +- VersionControlItems2 +- EventSubscriber +- WorkItemTrackingProvision +- ServiceEndpoints +- ServiceHooks +- Collection +- Proxy +- Plan +- Process +- AccountAdminSecurity +- Library +- Environment +- Project +- EventSubscription +- ProjectAnalysisLanguageMetrics +- Tagging +- MetaTask +- Iteration +- WorkItemQueryFolders +- Favorites +- Registry +- Graph +- ViewActivityPaneSecurity +- Job +- EventPublish +- WorkItemTracking +- StrongBox +- Server +- TestManagement +- SettingEntries +- BuildAdministration +- Location +- Boards +- OrganizationLevelData +- UtilizationPermissions +- WorkItemsHub +- WebPlatform +- VersionControlPrivileges +- Workspaces +- CrossProjectWidgetView +- WorkItemTrackingConfiguration +- DiscussionThreads +- BoardsExternalIntegration +- DataProvider +- Social +- Security +- IdentityPicker +- ServicingOrchestration +- Build +- DashboardsPrivileges +- CSS +- VersionControlItems + +#> +Enum DescriptorType { + + Analytics + AnalyticsViews + ReleaseManagement + AuditLog + Identity + WorkItemTrackingAdministration + DistributedTask + GitRepositories + VersionControlItems2 + EventSubscriber + WorkItemTrackingProvision + ServiceEndpoints + ServiceHooks + Collection + Proxy + Plan + Process + AccountAdminSecurity + Library + Environment + Project + EventSubscription + ProjectAnalysisLanguageMetrics + Tagging + MetaTask + Iteration + WorkItemQueryFolders + Favorites + Registry + Graph + ViewActivityPaneSecurity + Job + EventPublish + WorkItemTracking + StrongBox + Server + TestManagement + SettingEntries + BuildAdministration + Location + Boards + OrganizationLevelData + UtilizationPermissions + WorkItemsHub + WebPlatform + VersionControlPrivileges + Workspaces + CrossProjectWidgetView + WorkItemTrackingConfiguration + DiscussionThreads + BoardsExternalIntegration + DataProvider + Social + Security + IdentityPicker + ServicingOrchestration + Build + DashboardsPrivileges + CSS + VersionControlItems +} diff --git a/source/Enum/TokenType.ps1 b/source/Enum/TokenType.ps1 new file mode 100644 index 000000000..0c6163f04 --- /dev/null +++ b/source/Enum/TokenType.ps1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS +Defines the types of tokens used for authentication. + +.DESCRIPTION +The TokenType enumeration specifies the different types of tokens that can be used for authentication purposes. This includes Managed Identity, Personal Access Token, and Certificate. + +.ENUMERATION MEMBERS +ManagedIdentity + Represents a managed identity token used for authentication. + +PersonalAccessToken + Represents a personal access token used for authentication. + +Certificate + Represents a certificate used for authentication. + +.EXAMPLE +# To use the TokenType enumeration: +$tokenType = [TokenType]::ManagedIdentity + +.NOTES +This enumeration is part of the AzureDevOpsDsc module. +#> +enum TokenType +{ + ManagedIdentity + PersonalAccessToken + Certificate +} diff --git a/source/Examples/Authentication/1-NewAuthenticationPAT.ps1 b/source/Examples/Authentication/1-NewAuthenticationPAT.ps1 new file mode 100644 index 000000000..e30d5228c --- /dev/null +++ b/source/Examples/Authentication/1-NewAuthenticationPAT.ps1 @@ -0,0 +1,10 @@ +<# + .DESCRIPTION + This example shows how to authenticate with Azure DevOps using a Personal Access Token (PAT). +#> + +# Using New-AzDoAuthenticationProvider +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +# Using New-AzDoAuthenticationProvider +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -SecureStringPersonalAccessToken $SecureStringPAT diff --git a/source/Examples/Authentication/2-NewAuthenticationManagedIdentity.ps1 b/source/Examples/Authentication/2-NewAuthenticationManagedIdentity.ps1 new file mode 100644 index 000000000..b5cc0f777 --- /dev/null +++ b/source/Examples/Authentication/2-NewAuthenticationManagedIdentity.ps1 @@ -0,0 +1,8 @@ +<# + .DESCRIPTION + This example shows how to authenticate with Azure DevOps using a Managed Identity. +#> + +# Using New-AzDoAuthenticationProvider +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + diff --git a/source/Examples/Resources/AzDevOpsProject/1-AddProject.ps1 b/source/Examples/Resources/AzDevOpsProject/1-AddProject.ps1 index 2e7bc7eba..db2ce88e1 100644 --- a/source/Examples/Resources/AzDevOpsProject/1-AddProject.ps1 +++ b/source/Examples/Resources/AzDevOpsProject/1-AddProject.ps1 @@ -5,18 +5,11 @@ called 'Test Project' exists (or is added if it does not exist). #> +# Refer to Authentication\1-NewAuthenticationPAT.ps1 for the New-AzDoAuthenticationProvider command +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + Configuration Example { - param - ( - [Parameter(Mandatory = $true)] - [string] - $ApiUri, - - [Parameter(Mandatory = $true)] - [string] - $Pat - ) Import-DscResource -ModuleName 'AzureDevOpsDsc' @@ -25,13 +18,8 @@ Configuration Example AzDevOpsProject 'AddProject' { Ensure = 'Present' - - ApiUri = $ApiUri - Pat = $Pat - ProjectName = 'Test Project' ProjectDescription = 'A Test Project' - SourceControlType = 'Git' } diff --git a/source/Examples/Resources/AzDevOpsProject/2-UpdateProject.ps1 b/source/Examples/Resources/AzDevOpsProject/2-UpdateProject.ps1 index 41b201032..4fd713aea 100644 --- a/source/Examples/Resources/AzDevOpsProject/2-UpdateProject.ps1 +++ b/source/Examples/Resources/AzDevOpsProject/2-UpdateProject.ps1 @@ -6,18 +6,11 @@ updated to 'A Test Project with a new description'. #> +# Refer to Authentication\1-NewAuthenticationPAT.ps1 for the New-AzDoAuthenticationProvider command +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + Configuration Example { - param - ( - [Parameter(Mandatory = $true)] - [string] - $ApiUri, - - [Parameter(Mandatory = $true)] - [string] - $Pat - ) Import-DscResource -ModuleName 'AzureDevOpsDsc' @@ -26,14 +19,8 @@ Configuration Example AzDevOpsProject 'UpdateProject' { Ensure = 'Present' - - ApiUri = $ApiUri - Pat = $Pat - ProjectName = 'Test Project' ProjectDescription = 'A Test Project with a new description' # Updated property - - #SourceControlType = 'Git' # Note: Update of this property is not supported } diff --git a/source/Examples/Resources/AzDevOpsProject/3-DeleteProject.ps1 b/source/Examples/Resources/AzDevOpsProject/3-DeleteProject.ps1 index 890bbdc2c..83553347a 100644 --- a/source/Examples/Resources/AzDevOpsProject/3-DeleteProject.ps1 +++ b/source/Examples/Resources/AzDevOpsProject/3-DeleteProject.ps1 @@ -4,18 +4,11 @@ This example shows how to delete a project called 'Test Project'. #> +# Refer to Authentication\1-NewAuthenticationPAT.ps1 for the New-AzDoAuthenticationProvider command +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + Configuration Example { - param - ( - [Parameter(Mandatory = $true)] - [string] - $ApiUri, - - [Parameter(Mandatory = $true)] - [string] - $Pat - ) Import-DscResource -ModuleName 'AzureDevOpsDsc' @@ -25,10 +18,6 @@ Configuration Example AzDevOpsProject 'DeleteProject' { Ensure = 'Absent' # 'Absent' ensures this will be removed/deleted - - ApiUri = $ApiUri - Pat = $Pat - ProjectName = 'Test Project' # Identifies the name of the project to be deleted } diff --git a/source/Examples/Resources/AzDoGitPermission.md b/source/Examples/Resources/AzDoGitPermission.md new file mode 100644 index 000000000..3cb89a0ea --- /dev/null +++ b/source/Examples/Resources/AzDoGitPermission.md @@ -0,0 +1,195 @@ +# DSC AzDoGitPermission Resource + +# Syntax + +``` PowerShell +AzDoGitPermission [string] #ResourceName +{ + ProjectName = [String]$ProjectName + RepositoryName = [String]$RepositoryName + Permissions = [HashTable]$Permissions # See Permissions Syntax + [ Ensure = [String] {'Present', 'Absent'}] +} +``` + +## Permissions Syntax + +``` PowerShell +AzDoGitPermission/Permissions +{ + Identity = [String]$Identity # Syntax + # SYNTAX: '[ProjectName | OrganizationName]\ServicePrincipalName, UserPrincipalName, UserDisplayName, GroupDisplayName' + # EXAMPLE: '[TestProject]\UserName@email.com' + # EXAMPLE: '[SampleOrganizationName]\Project Collection Administrators' + Permission = [Hashtable[]]$Permissions # See 'Permission List" +} +``` + +## Permission Usage + +``` PowerShell +AzDoGitPermission/Permissions/Permission +{ + PermissionName|PermissionDisplayName = [String]$Name { 'Allow, Deny' } +} + +``` + +## Permission List + +> Either 'Name' or 'DisplayName' can be used, but we Strongly Recommend that you use 'Name' in your configuration. + +| Name | DisplayName | Values | Note | +| ------------- | ------------- | - | - | +|Administer | Administer | [ allow, deny ] | Not recommended. | +|GenericRead | Read | [ allow, deny ] | | +|GenericContribute | Contribute | [ allow, deny ] | | +|ForcePush | Force push (rewrite history, delete branches and tags) | [ allow, deny ] | | +|CreateBranch | Create branch |[ allow, deny ] | | +|CreateTag | Create tag | [ allow, deny ] | | +|ManageNote | Manage notes | [ allow, deny ] | | +|PolicyExempt | Bypass policies when pushing | [ allow, deny ] | | +|CreateRepository | Create repository | [ allow, deny ] | | +|DeleteRepository | Delete or disable repository | [ allow, deny ] | | +|RenameRepository | Rename repository | [ allow, deny ] | | +|EditPolicies | Edit policies | [ allow, deny ] | | +|RemoveOthersLocks | Remove others' locks | [ allow, deny ] | | +|ManagePermissions | Manage permissions | [ allow, deny ] | | +|PullRequestContribute | Contribute to pull requests | [ allow, deny ] | | +|PullRequestBypassPolicy | Bypass policies when completing pull requests | [ allow, deny ] | | +|ViewAdvSecAlerts | Advanced Security: view alerts | [ allow, deny ] | | +|DismissAdvSecAlerts | Advanced Security: manage and dismiss alerts | [ allow, deny ] | | +|ManageAdvSecScanning | Advanced Security: manage settings | [ allow, deny ] | | + +# Common Properties + +- __ProjectName__: The name of the Azure DevOps project. +- __RepositoryName__: The name of the Git repository within the project. +- __Permissions__: A HashTable that specifies the permissions to be set. Refer to: 'Permissions Syntax'. +- __Ensure__: Specifies whether the repository should exist. Defaults to 'Absent'. + +# Additional Information + +This resource allows you to manage Azure DevOps projects using Desired State Configuration (DSC). +It includes properties for specifying the project name, description, source control type, process template, and visibility. + +# Examples + +## Example 1: Sample Configuration using AzDoGitPermission Resource + +``` PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoGitPermission GitPermission { + Ensure = 'Present' + ProjectName = 'SampleProject' + RepositoryName = 'SampleGitRepository' + isInherited = $true + Permissions = @( + @{ + Identity = '[ProjectName]\GroupName' + Permissions = @{ + Read = 'Allow' + ManageNote = 'Allow' + Contribute = 'Deny' + } + } + ) + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose + +``` + +## Example 2: Sample Configuration using Invoke-DSCResource + +``` PowerShell +# Return the current configuration for AzDoGitPermission +# Ensure is not required +$properties = @{ + ProjectName = 'SampleProject' + RepositoryName = 'SampleGitRepository' + isInherited = $true + Permissions = @( + @{ + Identity = '[ProjectName]\GroupName' + Permissions = @{ + Read = 'Allow' + ManageNote = 'Allow' + Contribute = 'Deny' + } + } + ) +} + +Invoke-DSCResource -Name 'AzDoGitPermission' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 3: Sample Configuration to clear permissions for an identity within a group + +``` PowerShell +# Remove all group members from the group. +$properties = @{ + ProjectName = 'SampleProject' + RepositoryName = 'SampleGitRepository' + isInherited = $true + Permissions = @( + @{ + Identity = '[ProjectName]\GroupName' + Permissions = @{} + } + ) +} + +Invoke-DSCResource -Name 'AzDoGitPermission' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 4: Sample Configuration using AzDO-DSC-LCM + +``` YAML +parameters: {} + +variables: { + ProjectName: SampleProject, + RepositoryName: SampleRepository +} + +resources: + + - name: SampleGroup Permissions + type: AzureDevOpsDsc/AzDoGitPermission + dependsOn: + - AzureDevOpsDsc/AzDoProjectGroup/SampleGroupReadAccess + properties: + projectName: $ProjectName + RepositoryName: $RepositoryName + isInherited: false + Permissions: + - Identity: '[$ProjectName]\SampleGroupReadAccess' + Permission: + Read: "Allow" + ManageNote: "Allow" +``` + +LCM Initialization: + +``` PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params + +``` diff --git a/source/Examples/Resources/AzDoGitPermission/1-AddGitPermission.ps1 b/source/Examples/Resources/AzDoGitPermission/1-AddGitPermission.ps1 new file mode 100644 index 000000000..6ef82d744 --- /dev/null +++ b/source/Examples/Resources/AzDoGitPermission/1-AddGitPermission.ps1 @@ -0,0 +1,39 @@ +<# + .DESCRIPTION + This example shows how to ensure that the Git repository permissions. +#> + +# Refer to Authentication\1-NewAuthenticationPAT.ps1 for the New-AzDoAuthenticationProvider command +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGitPermission 'AddGitPermission' + { + Ensure = 'Present' + ProjectName = 'Test Project' + RepositoryName = 'Test Repository' + isInherited = $true + Permissions = @( + @{ + Identity = '[Project]\Contributors' + Permission = @{ + read = 'allow' + contribute = 'allow' + } + }, + @{ + Identity = '[Project]\Readers' + Permission = @{ + read = 'allow' + } + } + ) + } + } +} diff --git a/source/Examples/Resources/AzDoGitPermission/2-UpdateGitPermission.ps1 b/source/Examples/Resources/AzDoGitPermission/2-UpdateGitPermission.ps1 new file mode 100644 index 000000000..d8e4771ef --- /dev/null +++ b/source/Examples/Resources/AzDoGitPermission/2-UpdateGitPermission.ps1 @@ -0,0 +1,39 @@ +<# + .DESCRIPTION + This example shows how to update the Git repository permissions. +#> + +# Refer to Authentication\1-NewAuthenticationPAT.ps1 for the New-AzDoAuthenticationProvider command +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGitPermission 'UpdateGitPermission' + { + Ensure = 'Present' + ProjectName = 'Test Project' + RepositoryName = 'Test Repository' + isInherited = $true + Permissions = @( + @{ + Identity = '[Project]\Contributors' + Permission = @{ + read = 'allow' + contribute = 'deny' + } + }, + @{ + Identity = '[Project]\Readers' + Permission = @{ + read = 'deny' + } + } + ) + } + } +} diff --git a/source/Examples/Resources/AzDoGitPermission/3-DeleteAzDoGitPermission.ps1 b/source/Examples/Resources/AzDoGitPermission/3-DeleteAzDoGitPermission.ps1 new file mode 100644 index 000000000..374a5daa9 --- /dev/null +++ b/source/Examples/Resources/AzDoGitPermission/3-DeleteAzDoGitPermission.ps1 @@ -0,0 +1,27 @@ +<# + .DESCRIPTION + This example shows how to remove Git repository permissions. +#> + +# Refer to Authentication\1-NewAuthenticationPAT.ps1 for the New-AzDoAuthenticationProvider command +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGitPermission 'DeleteGitPermission' + { + Ensure = 'Present' + ProjectName = 'Test Project' + RepositoryName = 'Test Repository' + isInherited = $true + # Note: Permissions can be empty to remove all permissions + # Ensure = 'Absent' is not required. + Permissions = @() + } + } +} diff --git a/source/Examples/Resources/AzDoGitRepository.md b/source/Examples/Resources/AzDoGitRepository.md new file mode 100644 index 000000000..2dbe24069 --- /dev/null +++ b/source/Examples/Resources/AzDoGitRepository.md @@ -0,0 +1,83 @@ +# DSC AzDoGitRepository Resource + +## Syntax + +```PowerShell +AzDoGitRepository [string] #ResourceName +{ + ProjectName = [String]$ProjectName + RepositoryName = [String]$RepositoryName + [ SourceRepository = [String]$SourceRepository ] + [ Ensure = [String] {'Present', 'Absent'}] +} +``` + +## Permissions Syntax + +This resource does not directly manage permissions. It focuses on managing Git repositories within an Azure DevOps project. + +## Permission Usage + +Not applicable for this resource. + +## Permission List + +Not applicable for this resource. + +## Common Properties + +- __Ensure__: Specifies whether the repository should exist. Defaults to 'Absent'. +- __ProjectName__: The name of the Azure DevOps project. +- __RepositoryName__: The name of the Git repository within the project. +- __SourceRepository__: (Optional) The source repository from which to create the new repository. + +## Additional Information + +This resource allows you to manage Git repositories in Azure DevOps projects using Desired State Configuration (DSC). It includes properties for specifying the project name, repository name, and optionally a source repository. + +## Examples + +### Example 1: Create a Git Repository + +```PowerShell +Configuration Sample_AzDoGitRepository +{ + Import-DscResource -ModuleName AzDevOpsDsc + + Node localhost + { + AzDoGitRepository MyRepository + { + ProjectName = 'MySampleProject' + RepositoryName = 'MySampleRepository' + SourceRepository = 'TemplateRepository' + Ensure = 'Present' + } + } +} + +Sample_AzDoGitRepository -OutputPath 'C:\DSC\' +Start-DscConfiguration -Path 'C:\DSC\' -Wait -Verbose -Force +``` + +### Example 2: Remove a Git Repository + +```PowerShell +Configuration Remove_AzDoGitRepository +{ + Import-DscResource -ModuleName AzDevOpsDsc + + Node localhost + { + AzDoGitRepository MyRepository + { + ProjectName = 'MySampleProject' + RepositoryName = 'MySampleRepository' + Ensure = 'Absent' + } + } +} + +Remove_AzDoGitRepository -OutputPath 'C:\DSC\' +Start-DscConfiguration -Path 'C:\DSC\' -Wait -Verbose -Force +``` diff --git a/source/Examples/Resources/AzDoGitRepository/1-AddAzDoGitRepository.ps1 b/source/Examples/Resources/AzDoGitRepository/1-AddAzDoGitRepository.ps1 new file mode 100644 index 000000000..6ad778863 --- /dev/null +++ b/source/Examples/Resources/AzDoGitRepository/1-AddAzDoGitRepository.ps1 @@ -0,0 +1,22 @@ +<# + .DESCRIPTION + This example shows how to add the Git Repository +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGitRepository 'AddGitRepository' + { + Ensure = 'Present' + ProjectName = 'Test Project' + RepositoryName = 'Test Repository' + } + } +} diff --git a/source/Examples/Resources/AzDoGitRepository/2-UpdateAzDoGitRepository.ps1 b/source/Examples/Resources/AzDoGitRepository/2-UpdateAzDoGitRepository.ps1 new file mode 100644 index 000000000..64204ba85 --- /dev/null +++ b/source/Examples/Resources/AzDoGitRepository/2-UpdateAzDoGitRepository.ps1 @@ -0,0 +1,6 @@ +<# + .DESCRIPTION + This example shows how to update the Git Repository +#> + +# Not Currently Supported diff --git a/source/Examples/Resources/AzDoGitRepository/3-DeleteAzDoGitRepository.ps1 b/source/Examples/Resources/AzDoGitRepository/3-DeleteAzDoGitRepository.ps1 new file mode 100644 index 000000000..d05508d1c --- /dev/null +++ b/source/Examples/Resources/AzDoGitRepository/3-DeleteAzDoGitRepository.ps1 @@ -0,0 +1,22 @@ +<# + .DESCRIPTION + This example shows how to remove a Git Repository. +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGitRepository 'RemoveGitRepository' + { + Ensure = 'Absent' + ProjectName = 'Test Project' + RepositoryName = 'Test Repository' + } + } +} diff --git a/source/Examples/Resources/AzDoGroupMember.md b/source/Examples/Resources/AzDoGroupMember.md new file mode 100644 index 000000000..15c1f7172 --- /dev/null +++ b/source/Examples/Resources/AzDoGroupMember.md @@ -0,0 +1,127 @@ +# DSC AzDoGroupMember Resource + +## Syntax + +```PowerShell +AzDoGroupMember [string] #ResourceName +{ + GroupName = [String]$GroupName # [ProjectName|OrganizationName]\GroupName + # For GroupMember Syntax, refer to # GroupMembers Syntax + [ GroupMembers = [String[]]$GroupMembers ] +} +``` + +### GroupMembers Syntax + +``` PowerShell +{ + GroupMember = [String]$GroupMemberName # [ProjectName|OrganizationName]\GroupName +} +``` + +The following string represents the service accounts for the project collection in Azure DevOps Organization: + +```text +[ProjectName|AZDOOrganizationName]\Project Collection Service Accounts +``` + +- __[ProjectName|AZDOOrganizationName]__: The AzDO Project or Organizational Name. +- __Project Collection Service Accounts__: The Group Member Name. This can be a Group Name, Service Principal Name or Service Principle. + +#### Example + +If your Azure DevOps Organization name is `MyOrg`, the string would look like: + +```text +[MyOrg]\Project Collection Service Accounts +``` + +## Properties + +Common Properties: + +- __GroupName__: The name of the Azure DevOps group. +- __GroupMembers__: An array of members to be included in the Azure DevOps group. + +## Additional Information + +This resource is used to manage Azure DevOps group memberships using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps group and ensures that the group is configured according to those properties. + +## Examples + +### Example 1: Sample Configuration for Azure DevOps Group using AzDoGroupMember Resource + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoGroupMember GroupExample { + GroupName = 'MySampleGroup' + GroupMembers = @('user1@example.com', 'user2@example.com') + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Sample Configuration for Azure DevOps Group using Invoke-DSCResource + +```PowerShell +# Return the current configuration for AzDoGroupMember +$properties = @{ + GroupName = 'MySampleGroup' + GroupMembers = @('user1@example.com', 'user2@example.com') +} + +Invoke-DSCResource -Name 'AzDoGroupMember' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 3: Sample Configuration to remove/exclude an Azure DevOps Group using Invoke-DSCResource + +```PowerShell +# Remove the Azure DevOps Group and ensure that it is not recreated. +$properties = @{ + GroupName = 'MySampleGroup' + Ensure = 'Absent' +} + +Invoke-DSCResource -Name 'AzDoGroupMember' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 4: Sample Configuration using AzDO-DSC-LCM + +```YAML +parameters: {} + +variables: { + GroupName: SampleGroup, + GroupMembers: ['user1@example.com', 'user2@example.com'] +} + +resources: + + - name: Group + type: AzureDevOpsDsc/AzDoGroupMember + properties: + groupName: $GroupName + groupMembers: $GroupMembers +``` + +## LCM Initialization + +```PowerShell +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params +``` diff --git a/source/Examples/Resources/AzDoGroupMember/1-AddAzDoGroupMember.ps1 b/source/Examples/Resources/AzDoGroupMember/1-AddAzDoGroupMember.ps1 new file mode 100644 index 000000000..ac5bb23e8 --- /dev/null +++ b/source/Examples/Resources/AzDoGroupMember/1-AddAzDoGroupMember.ps1 @@ -0,0 +1,25 @@ +<# + .DESCRIPTION + This example shows how to add groups to a membership. +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGroupMember 'AddAzDoGroupMember' + { + Ensure = 'Present' + GroupName = '[ProjectName|OrganizationName]\GroupName' + GroupMembers = @( + '[Project]\Readers' + '[OrganizationName]\Project Collection Administrators' + ) + } + } +} diff --git a/source/Examples/Resources/AzDoGroupMember/2-UpdateAzDoGroupMember.ps1 b/source/Examples/Resources/AzDoGroupMember/2-UpdateAzDoGroupMember.ps1 new file mode 100644 index 000000000..1ce07d2ea --- /dev/null +++ b/source/Examples/Resources/AzDoGroupMember/2-UpdateAzDoGroupMember.ps1 @@ -0,0 +1,25 @@ +<# + .DESCRIPTION + TThis example shows how to update the group membership. +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGroupMember 'UpdateAzDoGroupMember' + { + Ensure = 'Present' + GroupName = '[ProjectName|OrganizationName]\GroupName' + GroupMembers = @( + '[Project]\New Group Name' + '[OrganizationName]\Project Collection Administrators' + ) + } + } +} diff --git a/source/Examples/Resources/AzDoGroupMember/3-DeleteAzDoGroupMember.ps1 b/source/Examples/Resources/AzDoGroupMember/3-DeleteAzDoGroupMember.ps1 new file mode 100644 index 000000000..f725538e9 --- /dev/null +++ b/source/Examples/Resources/AzDoGroupMember/3-DeleteAzDoGroupMember.ps1 @@ -0,0 +1,23 @@ +<# + .DESCRIPTION + This example shows how to remove a group membership. +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoGroupMember 'RemoveAzDoGroupMember' + { + Ensure = 'Present' + GroupName = '[ProjectName|OrganizationName]\GroupName' + # Ensure: Absent is not required. Zeroing out the membership is sufficent. + GroupMembers = @() + } + } +} diff --git a/source/Examples/Resources/AzDoGroupPermission.md b/source/Examples/Resources/AzDoGroupPermission.md new file mode 100644 index 000000000..4553d0331 --- /dev/null +++ b/source/Examples/Resources/AzDoGroupPermission.md @@ -0,0 +1,147 @@ +# AzDoGroupPermission Resource Documentation (Not Currently Supported) + +## Overview + +The `AzDoGroupPermission` resource is part of the Azure DevOps Desired State Configuration (DSC) module. It allows you to manage group permissions within an Azure DevOps project repository. This resource provides properties for specifying the group name, permission inheritance, and a list of permissions to be set. + +## Syntax + +```PowerShell +AzDoGroupPermission [string] #ResourceName +{ + GroupName = [String]$GroupName + [ isInherited = [Boolean]$isInherited ] + [ Permissions = [HashTable[]]$Permissions ] +} +``` + +### Properties + +- **GroupName**: The name of the Azure DevOps group. This property is mandatory. +- **isInherited**: Specifies whether the permissions should be inherited. Defaults to `$true`. +- **Permissions**: A HashTable array that specifies the permissions to be set for the group. Refer to the 'Permissions Syntax' section below. + +## Permissions Syntax + +```PowerShell +AzDoGroupPermission/Permissions +{ + Identity = [String]$Identity + # SYNTAX: '[ProjectName | OrganizationName]\ServicePrincipalName, UserPrincipalName, UserDisplayName, GroupDisplayName' + # ALTERNATIVE SYNTAX: 'this' Referring to the group. + # EXAMPLE: '[TestProject]\UserName@email.com' + # EXAMPLE: '[SampleOrganizationName]\Project Collection Administrators' + Permission = [Hashtable[]]$Permissions +} +``` + +### Permission Usage + +```PowerShell +AzDoGroupPermission/Permissions/Permission +{ + PermissionName|PermissionDisplayName = [String]$Name { 'Allow, Deny' } +} +``` + +### Permission List + +Either 'Name' or 'DisplayName' can be used: + +| Name | DisplayName | Values | Note | +|-------------------------|------------------------------------------------------|-----------------|------------------| +| Read | View identity information | [ allow, deny ] | | +| Write | Edit identity information | [ allow, deny ] | | +| Delete | Delete identity information | [ allow, deny ] | | +| ManageMembership | Manage group membership | [ allow, deny ] | | +| CreateScope | Create identity scopes | [ allow, deny ] | | +| RestoreScope | Restore identity scopes | [ allow, deny ] | | + +## Examples + +### Example 1: Set Group Permissions + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoGroupPermission GroupPermission { + GroupName = 'SampleGroup' + isInherited = $true + Permissions = @( + @{ + Identity = '[SampleProject]\SampleGroup' + Permissions = @{ + "Read" = 'Allow' + "Write" = 'Allow' + "Delete" = 'Deny' + } + } + ) + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Clear Group Permissions + +```PowerShell +# Remove all permissions from the group. +$properties = @{ + GroupName = 'SampleGroup' + isInherited = $true + Permissions = @( + @{ + Identity = '[SampleProject]\SampleGroup' + Permissions = @{} + } + ) +} + +Invoke-DSCResource -Name 'AzDoGroupPermission' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Methods + +### Get Method + +Retrieves the current state properties of the `AzDoGroupPermission` resource. + +```PowerShell +[AzDoGroupPermission] Get() +{ + return [AzDoGroupPermission]$($this.GetDscCurrentStateProperties()) +} +``` + +### GetDscCurrentStateProperties Method + +Returns the current state properties of the resource object. + +```PowerShell +hidden [Hashtable] GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) +{ + $properties = @{ + Ensure = [Ensure]::Absent + } + + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.GroupName = $CurrentResourceObject.GroupName + $properties.isInherited = $CurrentResourceObject.isInherited + $properties.Permissions = $CurrentResourceObject.Permissions + + Write-Verbose "[AzDoGroupPermission] Current state properties: $($properties | Out-String)" + + return $properties +} +``` + +This class inherits from the `AzDevOpsDscResourceBase` class, which provides the base functionality for DSC resources in the Azure DevOps DSC module. diff --git a/source/Examples/Resources/AzDoOrganizationGroup.md b/source/Examples/Resources/AzDoOrganizationGroup.md new file mode 100644 index 000000000..18e1d63f6 --- /dev/null +++ b/source/Examples/Resources/AzDoOrganizationGroup.md @@ -0,0 +1,94 @@ +# DSC AzDoOrganizationGroup Resource + +## Syntax + +```PowerShell +AzDoOrganizationGroup [string] #ResourceName +{ + GroupName = [String]$GroupName + [ GroupDescription = [String]$GroupDescription ] +} +``` + +## Properties + +### Common Properties + +- **GroupName**: The name of the organization group. This property is mandatory and serves as the key property for the resource. +- **GroupDescription**: A description of the organization group. + +## Additional Information + +This resource is used to manage Azure DevOps organization groups using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps organization group and ensures that the group is configured according to those properties. + +## Examples + +## Example 1: Sample Configuration using AzDoOrganizationGroup Resource + +``` PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoOrganizationGroup OrgGroup { + Ensure = 'Present' + GroupName = 'SampleGroup' + GroupDescription = 'This is a sample group!' + } + } +} + +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +## Example 2: Sample Configuration using Invoke-DSCResource + +``` PowerShell +# Return the current configuration for AzDoGitPermission +# Ensure is not required +$properties = @{ + GroupName = 'SampleGroup' + GroupDescription = 'This is a sample group!' +} + +Invoke-DSCResource -Name 'AzDoOrganizationGroup' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 3: Sample Configuration using AzDO-DSC-LCM + +``` YAML +parameters: {} + +variables: { + "PlaceHolder2": "PlaceHolder" +} + +resources: +- name: Team Leaders Organization Group + type: AzureDevOpsDsc/AzDoOrganizationGroup + properties: + GroupName: AZDO_TeamLeaders_Group + GroupDescription: Team Leaders Organization Group + +- name: Service Accounts Organization Group + type: AzureDevOpsDsc/AzDoOrganizationGroup + properties: + GroupName: AZDO_ServiceAccounts_Group + GroupDescription: Service Accounts Organization Group +``` + +LCM Initialization: + +``` PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params diff --git a/source/Examples/Resources/AzDoOrganizationGroup/1-AddAzDoOrganizationGroup.ps1 b/source/Examples/Resources/AzDoOrganizationGroup/1-AddAzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..d140be3da --- /dev/null +++ b/source/Examples/Resources/AzDoOrganizationGroup/1-AddAzDoOrganizationGroup.ps1 @@ -0,0 +1,22 @@ +<# + .DESCRIPTION + This example shows how to add a Orgnaization Group +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoOrganizationGroup 'AddAzDoOrganizationGroup' + { + Ensure = 'Present' + GroupName = 'Initial Group Name' + GroupDescription = 'Initial Description' + } + } +} diff --git a/source/Examples/Resources/AzDoOrganizationGroup/2-UpdateAzDoOrganizationGroup.ps1 b/source/Examples/Resources/AzDoOrganizationGroup/2-UpdateAzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..6938960b1 --- /dev/null +++ b/source/Examples/Resources/AzDoOrganizationGroup/2-UpdateAzDoOrganizationGroup.ps1 @@ -0,0 +1,22 @@ +<# + .DESCRIPTION + This example shows how to update a Orgnaization Group +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoOrganizationGroup 'UpdateAzDoOrganizationGroup' + { + Ensure = 'Present' + GroupName = 'Updated Group Name' + GroupDescription = 'Initial Description' + } + } +} diff --git a/source/Examples/Resources/AzDoOrganizationGroup/3-DeleteAzDoOrganizationGroup.ps1 b/source/Examples/Resources/AzDoOrganizationGroup/3-DeleteAzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..6cd179ad2 --- /dev/null +++ b/source/Examples/Resources/AzDoOrganizationGroup/3-DeleteAzDoOrganizationGroup.ps1 @@ -0,0 +1,21 @@ +<# + .DESCRIPTION + This example shows how to remove a Orgnaization Group +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoOrganizationGroup 'DeleteAzDoOrganizationGroup' + { + Ensure = 'Absent' + GroupName = 'Updated Group Name' + } + } +} diff --git a/source/Examples/Resources/AzDoProject.md b/source/Examples/Resources/AzDoProject.md new file mode 100644 index 000000000..dc3ee6168 --- /dev/null +++ b/source/Examples/Resources/AzDoProject.md @@ -0,0 +1,123 @@ +# DSC AzDoProject Resource + +# Syntax + +``` PowerShell +AzDoProject [string] #ResourceName +{ + ProjectName = [String]$ProjectName + [ Ensure = [String] {'Present', 'Absent'}] + [ ProjectDescription = [String]$ProjectDescription] + [ SourceControlType = [String] {'Git', 'Tfvc'}] + [ ProcessTemplate = [String] {'Agile', 'Scrum', 'CMMI', 'Basic'}] + [ Visibility = [String] {'Public', 'Private'}] +} +``` + +# Properties + +Common Properties: + +- __ProjectName__: The name of the Azure DevOps project. +- __ProjectDescription__: A description for the Azure DevOps project. +- __SourceControlType__: The type of source control (Git or Tfvc). Default is Git. +- __ProcessTemplate__: The process template to use (Agile, Scrum, CMMI, Basic). Default is Agile. +- __Visibility__: The visibility of the project (Public or Private). Default is Private. + +# Additional Information + +This resource is used to manage Azure DevOps projects using Desired State Configuration (DSC). +It allows you to define the properties of an Azure DevOps project and ensures that the project is configured according to those properties. + +# Examples + +## Example 1: Sample Configuration for Azure DevOps Project using AzDoProject Resource + +``` PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoProject ProjectExample { + Ensure = 'Present' + ProjectName = 'MySampleProject' + ProjectDescription = 'This is a sample Azure DevOps project.' + SourceControlType = 'Git' + ProcessTemplate = 'Agile' + Visibility = 'Private' + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose + +``` + +## Example 2: Sample Configuration for Azure DevOps Project using Invoke-DSCResource + +``` PowerShell +# Return the current configuration for AzDoProject +# Ensure is not required +$properties = @{ + ProjectName = 'MySameProject' + ProjectDiscription = 'This is a sample Azure DevOps project' + SourceControlType = 'Git' + ProcessTemplate = 'Agile' + Visibility = 'Private' +} + +Invoke-DSCResource -Name 'AzDoProject' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 3: Sample Configuration to remove/exclude an Azure DevOps Project using Invoke-DSCResource + +``` PowerShell +# Remove the Azure Devops Project and ensure that it is not recreated. +$properties = @{ + ProjectName = 'MySameProject' + Ensure = 'Absent' +} + +Invoke-DSCResource -Name 'AzDoProject' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 4: Sample Configuration using AzDO-DSC-LCM + +``` YAML +parameters: {} + +variables: { + ProjectName: SampleProject, + ProjectDescription: This is a SampleProject! +} + +resources: + + - name: Project + type: AzureDevOpsDsc/AzDoProject + properties: + projectName: $ProjectName + projectDescription: $ProjectDescription + visibility: private + SourceControlType: Git + ProcessTemplate: Agile +``` + +LCM Initialization: + +``` PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params + +``` diff --git a/source/Examples/Resources/AzDoProjectGroup.md b/source/Examples/Resources/AzDoProjectGroup.md new file mode 100644 index 000000000..eeeb148ba --- /dev/null +++ b/source/Examples/Resources/AzDoProjectGroup.md @@ -0,0 +1,102 @@ +# DSC AzDoProjectGroup Resource + +## Syntax + +```PowerShell +AzDoProjectGroup [string] #ResourceName +{ + GroupName = [String]$GroupName + ProjectName = [String]$ProjectName + [ GroupDescription = [String]$GroupDescription ] +} +``` + +## Properties + +### Common Properties + +- **GroupName**: The name of the project group. This property is mandatory and serves as the key property for the resource. +- **ProjectName**: The name of the Azure DevOps project associated with this group. This property is mandatory. +- **GroupDescription**: A description of the project group. + +## Additional Information + +This resource is used to manage Azure DevOps project groups using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps project group and ensures that the group is configured according to those properties. + +## Examples + +### Example 1: Sample Configuration using AzDoProjectGroup Resource + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoProjectGroup ProjectGroup { + Ensure = 'Present' + GroupName = 'SampleProjectGroup' + ProjectName = 'SampleProject' + GroupDescription = 'This is a sample project group!' + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Sample Configuration using Invoke-DSCResource + +```PowerShell +# Return the current configuration for AzDoProjectGroup +# Ensure is not required +$properties = @{ + GroupName = 'SampleProjectGroup' + ProjectName = 'SampleProject' + GroupDescription = 'This is a sample project group!' +} + +Invoke-DSCResource -Name 'AzDoProjectGroup' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 3: Sample Configuration using AzDO-DSC-LCM + +```YAML +parameters: {} + +variables: { + "PlaceHolder2": "PlaceHolder" +} + +resources: +- name: Team Leaders Project Group + type: AzureDevOpsDsc/AzDoProjectGroup + properties: + GroupName: AZDO_TeamLeaders_ProjectGroup + ProjectName: SampleProject + GroupDescription: Team Leaders Project Group + +- name: Service Accounts Project Group + type: AzureDevOpsDsc/AzDoProjectGroup + properties: + GroupName: AZDO_ServiceAccounts_ProjectGroup + ProjectName: SampleProject + GroupDescription: Service Accounts Project Group +``` + +LCM Initialization: + +```PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params +``` diff --git a/source/Examples/Resources/AzDoProjectGroup/1-AddAzDoProjectGroup.ps1 b/source/Examples/Resources/AzDoProjectGroup/1-AddAzDoProjectGroup.ps1 new file mode 100644 index 000000000..bc5114e9f --- /dev/null +++ b/source/Examples/Resources/AzDoProjectGroup/1-AddAzDoProjectGroup.ps1 @@ -0,0 +1,22 @@ +<# + .DESCRIPTION + This example shows how to add a Project Group +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoProjectGroup 'AddProjectGroup' { + Ensure = 'Present' + GroupName = 'SampleProjectGroup' + ProjectName = 'SampleProject' + GroupDescription = 'This is a sample project group!' + } + } +} diff --git a/source/Examples/Resources/AzDoProjectGroup/2-UpdateAzDoProjectGroup.ps1 b/source/Examples/Resources/AzDoProjectGroup/2-UpdateAzDoProjectGroup.ps1 new file mode 100644 index 000000000..1065ccc1f --- /dev/null +++ b/source/Examples/Resources/AzDoProjectGroup/2-UpdateAzDoProjectGroup.ps1 @@ -0,0 +1,22 @@ +<# + .DESCRIPTION + This example shows how to update a Project Group +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoProjectGroup 'UpdateProjectGroup' { + Ensure = 'Present' + GroupName = 'SampleProjectGroup' + ProjectName = 'SampleProject' + GroupDescription = 'New project group description' + } + } +} diff --git a/source/Examples/Resources/AzDoProjectGroup/3-DeleteAzDoProjectGroup.ps1 b/source/Examples/Resources/AzDoProjectGroup/3-DeleteAzDoProjectGroup.ps1 new file mode 100644 index 000000000..15b0a852a --- /dev/null +++ b/source/Examples/Resources/AzDoProjectGroup/3-DeleteAzDoProjectGroup.ps1 @@ -0,0 +1,21 @@ +<# + .DESCRIPTION + This example shows how to remove a Project Group +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + node localhost + { + AzDoProjectGroup 'RemoveProjectGroup' { + Ensure = 'Absent' + GroupName = 'SampleProjectGroup' + ProjectName = 'SampleProject' + } + } +} diff --git a/source/Examples/Resources/AzDoProjectServices.md b/source/Examples/Resources/AzDoProjectServices.md new file mode 100644 index 000000000..e44f21af3 --- /dev/null +++ b/source/Examples/Resources/AzDoProjectServices.md @@ -0,0 +1,111 @@ +# DSC AzDoProjectServices Resource + +## Syntax + +```PowerShell +AzDoProjectServices [string] #ResourceName +{ + ProjectName = [String]$ProjectName + [ GitRepositories = [String]$GitRepositories { 'Enabled' | 'Disabled' } ] + [ WorkBoards = [String]$WorkBoards { 'Enabled' | 'Disabled' } ] + [ BuildPipelines = [String]$BuildPipelines { 'Enabled' | 'Disabled' } ] + [ TestPlans = [String]$TestPlans { 'Enabled' | 'Disabled' } ] + [ AzureArtifact = [String]$AzureArtifact { 'Enabled' | 'Disabled' } ] +} +``` + +## Properties + +### Common Properties + +- **ProjectName**: The name of the Azure DevOps project. This property is mandatory and serves as the key property for the resource. +- **GitRepositories**: Specifies whether Git repositories are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **WorkBoards**: Specifies whether work boards are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **BuildPipelines**: Specifies whether build pipelines are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **TestPlans**: Specifies whether test plans are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **AzureArtifact**: Specifies whether Azure artifacts are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. + +## Additional Information + +This resource is used to manage Azure DevOps project services using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps project and ensures that the services are configured according to those properties. + +## Examples + +### Example 1: Sample Configuration using AzDoProjectServices Resource + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoProjectServices ProjectServices { + Ensure = 'Present' + ProjectName = 'SampleProject' + GitRepositories = 'Enabled' + WorkBoards = 'Enabled' + BuildPipelines = 'Enabled' + TestPlans = 'Enabled' + AzureArtifact = 'Enabled' + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Sample Configuration using Invoke-DSCResource + +```PowerShell +# Return the current configuration for AzDoProjectServices +# Ensure is not required +$properties = @{ + ProjectName = 'SampleProject' + GitRepositories = 'Enabled' + WorkBoards = 'Enabled' + BuildPipelines = 'Enabled' + TestPlans = 'Enabled' + AzureArtifact = 'Enabled' +} + +Invoke-DSCResource -Name 'AzDoProjectServices' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 3: Sample Configuration using AzDO-DSC-LCM + +```YAML +parameters: {} + +variables: { + "PlaceHolder2": "PlaceHolder" +} + +resources: +- name: Sample Project Services + type: AzureDevOpsDsc/AzDoProjectServices + properties: + ProjectName: SampleProject + GitRepositories: Enabled + WorkBoards: Enabled + BuildPipelines: Enabled + TestPlans: Enabled + AzureArtifact: Enabled +``` + +LCM Initialization: + +```PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params +``` + diff --git a/source/Examples/Resources/AzDoProjectServices/1-AddAzDoProjectServices.ps1 b/source/Examples/Resources/AzDoProjectServices/1-AddAzDoProjectServices.ps1 new file mode 100644 index 000000000..7ae9b07a2 --- /dev/null +++ b/source/Examples/Resources/AzDoProjectServices/1-AddAzDoProjectServices.ps1 @@ -0,0 +1,24 @@ +<# + .DESCRIPTION + This example shows how to add Project Services +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + Node localhost { + AzDoProjectServices 'AddProjectServices' { + Ensure = 'Present' + ProjectName = 'SampleProject' + GitRepositories = 'Enabled' + WorkBoards = 'Enabled' + BuildPipelines = 'Enabled' + TestPlans = 'Enabled' + AzureArtifact = 'Enabled' + } + } +} diff --git a/source/Examples/Resources/AzDoProjectServices/2-UpdateAzDoProjectServices.ps1 b/source/Examples/Resources/AzDoProjectServices/2-UpdateAzDoProjectServices.ps1 new file mode 100644 index 000000000..810327dc8 --- /dev/null +++ b/source/Examples/Resources/AzDoProjectServices/2-UpdateAzDoProjectServices.ps1 @@ -0,0 +1,24 @@ +<# + .DESCRIPTION + This example shows how to add Project Services +#> + +New-AzDoAuthenticationProvider -OrganizationName 'test-organization' -PersonalAccessToken 'my-pat' + +Configuration Example +{ + + Import-DscResource -ModuleName 'AzureDevOpsDsc' + + Node localhost { + AzDoProjectServices 'AddProjectServices' { + Ensure = 'Present' + ProjectName = 'SampleProject' + GitRepositories = 'Enabled' + WorkBoards = 'Disabled' + BuildPipelines = 'Disabled' + TestPlans = 'Disabled' + AzureArtifact = 'Disabled' + } + } +} diff --git a/source/Examples/Resources/AzDoProjectServices/3-DeleteAzDoProjectServices.ps1 b/source/Examples/Resources/AzDoProjectServices/3-DeleteAzDoProjectServices.ps1 new file mode 100644 index 000000000..11eaf9ff3 --- /dev/null +++ b/source/Examples/Resources/AzDoProjectServices/3-DeleteAzDoProjectServices.ps1 @@ -0,0 +1,6 @@ +<# + .DESCRIPTION + This example shows how to remove a Project Services. +#> + +# Not supported. See Update Project Services. diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Classes/000.CacheItem.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Classes/000.CacheItem.ps1 new file mode 100644 index 000000000..abb704c56 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Classes/000.CacheItem.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS +Represents a cache item with a key, value, and creation timestamp. + +.DESCRIPTION +The CacheItem class is used to store a key-value pair along with the timestamp when the item was created. +It ensures that the key is not null or empty upon instantiation. + +.PARAMETER Key +The key associated with the cache item. It must be a non-empty string. + +.PARAMETER Value +The value associated with the cache item. It can be any object. + +.PARAMETER created +The timestamp when the cache item was created. It is automatically set to the current date and time upon instantiation. + +.CONSTRUCTOR +CacheItem([string] $Key, [object] $Value) +Creates a new instance of the CacheItem class with the specified key and value. +Throws an exception if the key is null or empty. + +.EXAMPLE +$cacheItem = [CacheItem]::new("exampleKey", "exampleValue") +Creates a new CacheItem instance with the key "exampleKey" and the value "exampleValue". + +.NOTES +Author: Michael Zanatta +#> + +class CacheItem +{ + [string] $Key + [object] $Value + [datetime] $created + + CacheItem([string] $Key, [object] $Value) + { + # The Key Can't be empty + if (-not $Key) + { + throw "Key cannot be null or empty." + } + + $this.Key = $Key + $this.Value = $Value + $this.created = Get-Date + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.Get-AzDevOpsApiResourceUri.Tests.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACE/Empty.txt similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.Get-AzDevOpsApiResourceUri.Tests.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACE/Empty.txt diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsACL.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsACL.ps1 new file mode 100644 index 000000000..b4c2cbd64 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsACL.ps1 @@ -0,0 +1,36 @@ +function Get-DevOpsACL +{ + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter(Mandatory = $true)] + [String]$SecurityDescriptorId, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + # Construct the URL for the API call + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/accesscontrollists/{1}?api-version={2}' -f $OrganizationName, $SecurityDescriptorId, $ApiVersion + Method = 'Get' + } + + # Invoke the REST API call + $ACLList = Invoke-AzDevOpsApiRestMethod @params + + if (($null -eq $ACLList.value) -or ($ACLList.count -eq 0)) + { + return $null + } + + # + # Cache the ACL List. Use the SecurityDescriptorId as the key + Add-CacheItem -Key $SecurityDescriptorId -Value $ACLList.value -Type 'LiveACLList' + Export-CacheObject -CacheType 'LiveACLList' -Content $Global:AzDoLiveACLList + + return $ACLList.value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsDescriptorIdentity.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsDescriptorIdentity.ps1 new file mode 100644 index 000000000..192d20ad8 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsDescriptorIdentity.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS +Retrieves the identity associated with a given subject descriptor in Azure DevOps. + +.DESCRIPTION +The Get-DevOpsDescriptorIdentity function retrieves the identity associated with a given subject descriptor in Azure DevOps. It makes a REST API call to the Azure DevOps API to fetch the identity information. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. + +.PARAMETER SubjectDescriptor +The subject descriptor of the identity to retrieve. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. If not specified, the default API version will be used. + +.EXAMPLE +Get-DevOpsDescriptorIdentity -OrganizationName "MyOrg" -SubjectDescriptor "subject:abcd1234" + +This example retrieves the identity associated with the subject descriptor "subject:abcd1234" in the Azure DevOps organization "MyOrg". + +#> +Function Get-DevOpsDescriptorIdentity +{ + [CmdletBinding(DefaultParameterSetName = 'Default')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Default')] + [Parameter(Mandatory = $true, ParameterSetName = 'Descriptors')] + [string]$OrganizationName, + + [Parameter(Mandatory = $true, ParameterSetName = 'Default')] + [String]$SubjectDescriptor, + + [Parameter(Mandatory = $true, ParameterSetName = 'Descriptors')] + [String]$Descriptor, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Descriptors')] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + # Determine the query parameter based on the parameter set + if ($SubjectDescriptor) + { + $query = "subjectDescriptors=$SubjectDescriptor" + } + else + { + $query = "descriptors=$Descriptor" + } + + # + # Construct the URL for the API call + $params = @{ + Uri = 'https://vssps.dev.azure.com/{0}/_apis/identities?{1}&api-version={2}' -f $OrganizationName, $query, $ApiVersion + Method = 'Get' + } + + # Invoke the REST API call + $identity = Invoke-AzDevOpsApiRestMethod @params + + if (($null -eq $identity.value) -or ($identity.count -gt 1)) + { + return $null + } + + return $identity.value + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.New-AzDevOpsApiResource.Tests.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ApiResource/Empty.txt similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.New-AzDevOpsApiResource.Tests.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ApiResource/Empty.txt diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Remove-AzDoPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Remove-AzDoPermission.ps1 new file mode 100644 index 000000000..07e032f87 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Remove-AzDoPermission.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS +Removes access control lists (ACLs) for a specified token in an Azure DevOps organization. + +.DESCRIPTION +The Remove-AzDoPermission function removes ACLs for a specified token within a given security namespace in an Azure DevOps organization. It constructs the appropriate API endpoint and invokes a REST method to delete the ACLs. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. + +.PARAMETER SecurityNamespaceID +The ID of the security namespace. + +.PARAMETER TokenName +The name of the token for which the ACLs should be removed. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. If not specified, the default API version is used. + +.EXAMPLE +Remove-AzDoPermission -OrganizationName "MyOrg" -SecurityNamespaceID "12345" -TokenName "MyToken" + +This example removes the ACLs for the token "MyToken" in the security namespace with ID "12345" within the "MyOrg" organization. + +.NOTES +This function uses the Invoke-AzDevOpsApiRestMethod function to perform the REST API call. Ensure that the necessary permissions are in place to delete ACLs in the specified Azure DevOps organization. + +#> +Function Remove-AzDoPermission +{ + param( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter(Mandatory = $true)] + [string]$SecurityNamespaceID, + + [Parameter(Mandatory = $true)] + [string]$TokenName, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + Write-Verbose "[Remove-AzDoPermission] Started." + + # Define a hashtable to store parameters for the Invoke-AzDevOpsApiRestMethod function. + $params = @{ + <# + Construct the Uri using string formatting with the -f operator. + It includes the API endpoint, group identity, member identity, and the API version. + #> + Uri = 'https://dev.azure.com/{0}/_apis/accesscontrollists/{1}?tokens={2}&recurse=False&api-version={3}' -f $OrganizationName, + $SecurityNamespaceID, + $TokenName, + $ApiVersion + # Set the method to DELETE. + Method = 'DELETE' + } + + try + { + <# + Call the Invoke-AzDevOpsApiRestMethod function with the parameters defined above. + The "@" symbol is used to pass the hashtable as splatting parameters. + #> + Write-Verbose "[Remove-AzDoPermission] Attempting to invoke REST method to remove ACLs." + $member = Invoke-AzDevOpsApiRestMethod @params + + if ($member -ne $true) + { + Write-Error "[Remove-AzDoPermission] Failed to remove ACLs." + } + else + { + Write-Verbose "[Remove-AzDoPermission] ACLs removed successfully." + } + + } + catch + { + # If an exception occurs, write an error message to the console with details about the issue. + Write-Error "[Remove-AzDoPermission] Failed to add member to group: $($_.Exception.Message)" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Set-AzDoPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Set-AzDoPermission.ps1 new file mode 100644 index 000000000..84c593aec --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Set-AzDoPermission.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS +Sets Azure DevOps permissions by invoking a REST API method. + +.DESCRIPTION +The Set-AzDoPermission function sets permissions in Azure DevOps by sending a POST request to the specified API endpoint. +It constructs the URI using the organization name, security namespace ID, and API version. The function serializes the +Access Control Lists (ACLs) and sends them in the body of the request. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. + +.PARAMETER SecurityNamespaceID +The ID of the security namespace. + +.PARAMETER SerializedACLs +The serialized Access Control Lists (ACLs) to be set. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. Defaults to the value returned by Get-AzDevOpsApiVersion -Default. + +.EXAMPLE +Set-AzDoPermission -OrganizationName "MyOrg" -SecurityNamespaceID "12345" -SerializedACLs $acls + +This example sets the permissions for the specified organization and security namespace using the provided ACLs. + +.NOTES +The function uses the Invoke-AzDevOpsApiRestMethod to send the request. If an error occurs during the request, +an error message is written to the console. +#> + +Function Set-AzDoPermission +{ + param( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter(Mandatory = $true)] + [string]$SecurityNamespaceID, + + [Parameter(Mandatory = $true)] + [Object]$SerializedACLs, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + Write-Verbose "[Set-AzDoPermission] Started." + + # Define a hashtable to store parameters for the Invoke-AzDevOpsApiRestMethod function. + + $params = @{ + <# + Construct the Uri using string formatting with the -f operator. + It includes the API endpoint, group identity, member identity, and the API version. + #> + Uri = 'https://dev.azure.com/{0}/_apis/accesscontrollists/{1}?api-version={2}' -f $OrganizationName, + $SecurityNamespaceID, + $ApiVersion + # Set the method to PUT. + Method = 'POST' + # Set the body of the request to the serialized ACLs. + Body = $SerializedACLs | ConvertTo-Json -Depth 4 + } + + Write-Verbose "[Set-AzDoPermission] Body: $($params.Body)" + + try + { + <# + Call the Invoke-AzDevOpsApiRestMethod function with the parameters defined above. + The "@" symbol is used to pass the hashtable as splatting parameters. + #> + Write-Verbose "[Set-AzDoPermission] Attempting to invoke REST method to set ACLs." + $null = Invoke-AzDevOpsApiRestMethod @params + + } + catch + { + # If an exception occurs, write an error message to the console with details about the issue. + Write-Error "[Set-AzDoPermission] Failed to set ACLs: $($_.Exception.Message)" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGitRepository.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGitRepository.ps1 new file mode 100644 index 000000000..f70d7b53c --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGitRepository.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS +Retrieves a list of Git repositories from an Azure DevOps project. + +.DESCRIPTION +The List-DevOpsGitRepository function invokes the Azure DevOps REST API to retrieve a list of Git repositories for a specified organization and project. The function returns the list of repositories if available. + +.PARAMETER OrganizationName +Specifies the name of the Azure DevOps organization. This parameter is mandatory. + +.PARAMETER ProjectName +Specifies the name of the Azure DevOps project. This parameter is mandatory. + +.PARAMETER ApiVersion +Specifies the API version to use when making the request. If not provided, the default API version is used. + +.RETURNS +Returns a list of Git repositories if available; otherwise, returns $null. + +.EXAMPLE +PS> List-DevOpsGitRepository -OrganizationName "MyOrg" -ProjectName "MyProject" + +.NOTES +This function requires the Azure DevOps module to be installed and configured. +#> + +function List-DevOpsGitRepository +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter(Mandatory = $true)] + [String]$ProjectName, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = "https://dev.azure.com/$OrganizationName/$ProjectName/_apis/git/repositories" + Method = 'Get' + } + + # Invoke the Rest API to get the groups + $repositories = Invoke-AzDevOpsApiRestMethod @params + + # Return the groups from the cache + if ($null -eq $repositories.value) + { + return $null + } + + # + # Return the groups from the cache + return $repositories.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroupMembers.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroupMembers.ps1 new file mode 100644 index 000000000..1913744e3 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroupMembers.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS +Retrieves the members of a specified Azure DevOps group. + +.DESCRIPTION +The List-DevOpsGroupMembers function retrieves the members of a specified Azure DevOps group by invoking the Azure DevOps REST API. +It requires the organization name and group descriptor as mandatory parameters. Optionally, an API version can be specified. + +.PARAMETER Organization +The name of the Azure DevOps organization. + +.PARAMETER GroupDescriptor +The descriptor of the Azure DevOps group whose members are to be retrieved. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. If not specified, the default API version is used. + +.RETURNS +Returns the members of the specified Azure DevOps group. If no members are found, returns $null. + +.EXAMPLE +$list = List-DevOpsGroupMembers -Organization "myOrg" -GroupDescriptor "vssgp.Uy1zNTk3NzA3LTY3NzgtNDk4NC04YjE4LTYxZDE3YjY2YjA3Nw==" +This example retrieves the members of the specified Azure DevOps group in the "myOrg" organization. + +#> +function List-DevOpsGroupMembers +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Organization, + [Parameter(Mandatory = $true)] + [String] + $GroupDescriptor, + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = 'https://vssps.dev.azure.com/{0}/_apis/graph/Memberships/{1}?direction=down' -f $Organization, $GroupDescriptor + Method = 'Get' + } + + # + # Invoke the Rest API to get the groups + $membership = Invoke-AzDevOpsApiRestMethod @params + + # Return the groups from the cache + if ($null -eq $membership.value) + { + return $null + } + + # + # Return the groups from the cache + return $membership.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroups.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroups.ps1 new file mode 100644 index 000000000..683ecb4ea --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroups.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + Retrieves a list of DevOps groups for a specified organization. + +.DESCRIPTION + This function invokes the Azure DevOps REST API to retrieve a list of groups within a specified organization. + It uses the provided organization name and an optional API version to make the request. + +.PARAMETER Organization + The name of the Azure DevOps organization for which to retrieve the groups. + This parameter is mandatory. + +.PARAMETER ApiVersion + The version of the Azure DevOps API to use for the request. + If not specified, the default API version is used. + +.OUTPUTS + System.Object + Returns an array of groups if found, otherwise returns $null. + +.EXAMPLE + List-DevOpsGroups -Organization "myOrganization" + + This example retrieves the list of DevOps groups for the organization "myOrganization". +#> +Function List-DevOpsGroups +{ + [CmdletBinding()] + [OutputType([System.Object])] + Param + ( + [Parameter(Mandatory = $true)] + [string] + $Organization, + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/groups" + Method = 'Get' + } + + # Invoke the Rest API to get the groups + $groups = Invoke-AzDevOpsApiRestMethod @params + + # Return the groups from the cache + if ($null -eq $groups.value) + { + return $null + } + + # Return the groups from the cache + return $groups.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProcess.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProcess.ps1 new file mode 100644 index 000000000..0d0f0352f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProcess.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS + Retrieves the list of DevOps processes for a specified organization. + +.DESCRIPTION + This function invokes the Azure DevOps REST API to retrieve the list of DevOps processes for a given organization. + It uses the specified API version or defaults to the version returned by Get-AzDevOpsApiVersion. + +.PARAMETER Organization + The name of the Azure DevOps organization for which to retrieve the processes. + This parameter is mandatory. + +.PARAMETER ApiVersion + The version of the API to use. If not specified, the default version is used as returned by Get-AzDevOpsApiVersion. + +.OUTPUTS + System.Object + Returns the list of DevOps processes for the specified organization. + +.EXAMPLE + List-DevOpsProcess -Organization "myOrganization" + + This example retrieves the list of DevOps processes for the organization "myOrganization" using the default API version. + +.EXAMPLE + List-DevOpsProcess -Organization "myOrganization" -ApiVersion "6.0" + + This example retrieves the list of DevOps processes for the organization "myOrganization" using API version "6.0". +#> +Function List-DevOpsProcess +{ + [CmdletBinding()] + [OutputType([System.Object])] + Param + ( + [Parameter(Mandatory = $true)] + [string] + $Organization, + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/process/processes?api-version={1}' -f $Organization, $ApiVersion + Method = 'Get' + } + + # Invoke the Rest API to get the groups + $groups = Invoke-AzDevOpsApiRestMethod @params + # Return the groups from the cache + if ($null -eq $groups.value) + { + return $null + } + + # + # Return the groups from the cache + return $groups.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProjects.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProjects.ps1 new file mode 100644 index 000000000..31f426a1b --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProjects.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + Retrieves a list of DevOps projects for a specified organization. + +.DESCRIPTION + This function invokes the Azure DevOps REST API to retrieve a list of projects + for a given organization. It uses the specified API version or defaults to the + version obtained from Get-AzDevOpsApiVersion. + +.PARAMETER OrganizationName + The name of the Azure DevOps organization for which to list projects. + This parameter is mandatory. + +.PARAMETER ApiVersion + The version of the Azure DevOps API to use. If not specified, the default + version is obtained from Get-AzDevOpsApiVersion. + +.RETURNS + An array of project objects if projects are found, otherwise $null. + +.EXAMPLE + $projects = List-DevOpsProjects -OrganizationName "myOrganization" + This example retrieves the list of projects for the organization "myOrganization". + +.NOTES + This function requires the Az.DevOps module to be installed and imported. +#> + +function List-DevOpsProjects +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = "https://dev.azure.com/$OrganizationName/_apis/projects" + Method = 'Get' + } + + # Invoke the Rest API to get the groups + $groups = Invoke-AzDevOpsApiRestMethod @params + + if ($null -eq $groups.value) + { + return $null + } + + # Return the groups from the cache + return $groups.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsSecurityNamespaces.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsSecurityNamespaces.ps1 new file mode 100644 index 000000000..8dfe2ee61 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsSecurityNamespaces.ps1 @@ -0,0 +1,48 @@ +<# +.SYNOPSIS + Retrieves the security namespaces for a specified Azure DevOps organization. + +.DESCRIPTION + The List-DevOpsSecurityNamespaces function invokes the Azure DevOps REST API to retrieve the security namespaces for a given organization. + It returns the namespaces if available, otherwise returns null. + +.PARAMETER OrganizationName + The name of the Azure DevOps organization for which to retrieve the security namespaces. + +.EXAMPLE + List-DevOpsSecurityNamespaces -OrganizationName "Contoso" + + This example retrieves the security namespaces for the "Contoso" Azure DevOps organization. + +.NOTES + This function uses the Invoke-AzDevOpsApiRestMethod cmdlet to make the REST API call. +#> +Function List-DevOpsSecurityNamespaces +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [String]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "[List-DevOpsSecurityNamespaces] Started." + + # Params + $params = @{ + Uri = "https://dev.azure.com/$OrganizationName/_apis/securitynamespaces/" + Method = 'Get' + } + + # Invoke the Rest API to get the groups + $namespaces = Invoke-AzDevOpsApiRestMethod @params + + if ($null -eq $namespaces.value) + { + return $null + } + + # Return the groups from the cache + return $namespaces.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsServicePrinciples.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsServicePrinciples.ps1 new file mode 100644 index 000000000..bc54343ea --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsServicePrinciples.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS +Retrieves a list of service principals from an Azure DevOps organization. + +.DESCRIPTION +The List-DevOpsServicePrinciples function calls the Azure DevOps REST API to retrieve a list of service principals for a specified organization. The function requires the organization name and optionally accepts an API version. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization from which to retrieve the service principals. This parameter is mandatory. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. If not specified, the default API version is used. + +.EXAMPLE +PS> List-DevOpsServicePrinciples -OrganizationName "myOrganization" + +This example retrieves the list of service principals for the organization named "myOrganization". + +.EXAMPLE +PS> List-DevOpsServicePrinciples -OrganizationName "myOrganization" -ApiVersion "6.0" + +This example retrieves the list of service principals for the organization named "myOrganization" using API version "6.0". + +.RETURNS +The function returns a list of service principals if available; otherwise, it returns $null. + +#> +Function List-DevOpsServicePrinciples +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = "https://vssps.dev.azure.com/$OrganizationName/_apis/graph/serviceprincipals" + Method = 'Get' + } + + # + # Invoke the Rest API to get the groups + $serviceprincipals = Invoke-AzDevOpsApiRestMethod @params + + if ($null -eq $serviceprincipals.value) + { + return $null + } + + # Return the groups from the cache + return $serviceprincipals.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-UserCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-UserCache.ps1 new file mode 100644 index 000000000..f1b761b75 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-UserCache.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS +Retrieves the list of users from the Azure DevOps organization. + +.DESCRIPTION +The List-UserCache function invokes the Azure DevOps REST API to retrieve the list of users +for a specified organization. It uses the provided organization name and an optional API version +to make the request. If no API version is specified, it defaults to the version returned by +the Get-AzDevOpsApiVersion function. + +.PARAMETER OrganizationName +Specifies the name of the Azure DevOps organization from which to retrieve the list of users. +This parameter is mandatory. + +.PARAMETER ApiVersion +Specifies the version of the Azure DevOps API to use. If not provided, the default version +returned by the Get-AzDevOpsApiVersion function is used. + +.RETURNS +Returns the list of users from the specified Azure DevOps organization. If no users are found, +returns $null. + +.EXAMPLE +PS> List-UserCache -OrganizationName "myOrganization" +Retrieves the list of users from the "myOrganization" Azure DevOps organization using the default API version. + +.EXAMPLE +PS> List-UserCache -OrganizationName "myOrganization" -ApiVersion "6.0" +Retrieves the list of users from the "myOrganization" Azure DevOps organization using API version 6.0. +#> +function List-UserCache +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = "https://vssps.dev.azure.com/$OrganizationName/_apis/graph/users" + Method = 'Get' + } + + # Invoke the Rest API to get the groups + $users = Invoke-AzDevOpsApiRestMethod @params + + if ($null -eq $users.value) + { + return $null + } + + # Return the groups from the cache + return $users.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/New-GitRepository.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/New-GitRepository.ps1 new file mode 100644 index 000000000..24b721bc3 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/New-GitRepository.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS +Creates a new Git repository in an Azure DevOps project. + +.DESCRIPTION +The `New-GitRepository` function creates a new Git repository within a specified Azure DevOps project. +It uses the Azure DevOps REST API to perform the operation. + +.PARAMETER ApiUri +The base URI of the Azure DevOps API. + +.PARAMETER Project +The project object containing the project details. This should include at least the project name and ID. + +.PARAMETER RepositoryName +The name of the new Git repository to be created. + +.PARAMETER SourceRepository +(Optional) The source repository to use for the new repository. + +.PARAMETER ApiVersion +(Optional) The API version to use for the Azure DevOps REST API. Defaults to the version returned by `Get-AzDevOpsApiVersion -Default`. + +.OUTPUTS +System.Management.Automation.PSObject[] +Returns the created repository object if successful. + +.EXAMPLE +PS> New-GitRepository -ApiUri "https://dev.azure.com/organization" -Project $project -RepositoryName "NewRepo" + +This example creates a new Git repository named "NewRepo" in the specified Azure DevOps project. + +.NOTES +This function requires the `Invoke-AzDevOpsApiRestMethod` function to be defined and available in the session. +#> +Function New-GitRepository +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('URI')] + [System.String]$ApiUri, + + [Parameter(Mandatory = $true)] + [Alias('Name')] + [Object]$Project, + + [Parameter(Mandatory = $true)] + [Alias('Repository')] + [System.String]$RepositoryName, + + [Parameter()] + [Alias('Source')] + [System.String]$SourceRepository, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + Write-Verbose "[New-GitRepository] Creating new repository '$($RepositoryName)' in project '$($Project.name)'" + + # Define parameters for creating a new DevOps group + $params = @{ + ApiUri = '{0}/{1}/_apis/git/repositories?api-version={2}' -f $ApiUri, $Project.name, $ApiVersion + Method = 'POST' + ContentType = 'application/json' + Body = @{ + name = $RepositoryName + project = @{ + id = $Project.id + } + } | ConvertTo-Json + } + + # Try to invoke the REST method to create the group and return the result + try + { + $repo = Invoke-AzDevOpsApiRestMethod @params + Write-Verbose "[New-GitRepository] Repository Created: '$($repo.name)'" + return $repo + } + # Catch any exceptions and write an error message + catch + { + Write-Error "[New-GitRepository] Failed to Create Repository: $_" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/Remove-GitRepository.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/Remove-GitRepository.ps1 new file mode 100644 index 000000000..6e07225d3 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/Remove-GitRepository.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS +Removes a Git repository from an Azure DevOps project. + +.DESCRIPTION +The Remove-GitRepository function removes a specified Git repository from a given Azure DevOps project using the provided API URI and version. + +.PARAMETER ApiUri +The base URI of the Azure DevOps API. + +.PARAMETER Project +The project from which the repository will be removed. This should be an object containing the project details. + +.PARAMETER Repository +The repository to be removed. This should be an object containing the repository details. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. If not specified, the default version will be used. + +.EXAMPLE +Remove-GitRepository -ApiUri "https://dev.azure.com/organization" -Project $project -Repository $repository + +.NOTES +This function uses the Invoke-AzDevOpsApiRestMethod cmdlet to perform the REST API call to remove the repository. +#> +Function Remove-GitRepository +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('URI')] + [System.String]$ApiUri, + + [Parameter(Mandatory = $true)] + [Alias('Name')] + [Object]$Project, + + [Parameter(Mandatory = $true)] + [Alias('Repo')] + [Object]$Repository, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + Write-Verbose "[Remove-GitRepository] Removing repository '$($Repository.Name)' in project '$($Project.name)'" + + # Define parameters for creating a new DevOps group + $params = @{ + ApiUri = '{0}/{1}/_apis/git/repositories/{2}?api-version={3}' -f $ApiUri, $Project.name, $Repository.id , $ApiVersion + Method = 'Delete' + } + + # Try to invoke the REST method to create the group and return the result + try + { + $null = Invoke-AzDevOpsApiRestMethod @params + return + } + # Catch any exceptions and write an error message + catch + { + Write-Error "[Remove-GitRepository] Failed to Create Repository: $_" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/New-DevOpsGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/New-DevOpsGroup.ps1 new file mode 100644 index 000000000..18152f0a7 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/New-DevOpsGroup.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS +Creates a new group in Azure DevOps. + +.DESCRIPTION +The New-DevOpsGroup function creates a new group in Azure DevOps using the specified parameters. + +.PARAMETER ApiUri +The URI of the Azure DevOps API. + +.PARAMETER GroupName +The name of the group to create. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. If not specified, the default version will be used. + +.PARAMETER ProjectScopeDescriptor +The scope descriptor of the project. If specified, the group will be created within the specified project scope. + +.OUTPUTS +System.Management.Automation.PSObject + +.EXAMPLE +New-DevOpsGroup -ApiUri "https://dev.azure.com/myorganization" -GroupName "MyGroup" + +Creates a new group named "MyGroup" in Azure DevOps using the specified API URI. + +.EXAMPLE +New-DevOpsGroup -ApiUri "https://dev.azure.com/myorganization" -GroupName "MyGroup" -ProjectScopeDescriptor "vstfs:///Classification/TeamProject/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +Creates a new group named "MyGroup" in Azure DevOps within the specified project scope. + +#> +# Define a function to create a new Azure DevOps Group +Function New-DevOpsGroup +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject])] + param + ( + # Parameter attribute marks this as a mandatory parameter that the user must supply when calling the function. + [Parameter(Mandatory = $true)] + [string] + $ApiUri, # The URI for the Azure DevOps API. + + # Mandatory parameter for the group name + [Parameter(Mandatory = $true)] + [string] + $GroupName, + + # Optional parameter for the group description with a default value of null + [Parameter()] + [String] + $GroupDescription = $null, + + # Optional parameter for the API version with a default value obtained from the Get-AzDevOpsApiVersion function + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), + + # Optional parameter for the project scope descriptor + [Parameter()] + [String] + $ProjectScopeDescriptor + ) + + # Hashtable to hold parameters for the API request + $params = @{ + Uri = '{0}/_apis/graph/groups?api-version={1}' -f $ApiUri, $ApiVersion + Method = 'Post' + ContentType = 'application/json' + Body = @{ + displayName = $GroupName + description = $GroupDescription + } | ConvertTo-Json + } + + # If ProjectScopeDescriptor is provided, modify the URI to include it + if ($ProjectScopeDescriptor) + { + $params.Uri = '{0}/_apis/graph/groups?scopeDescriptor={1}&api-version={2}' -f $ApiUri, $ProjectScopeDescriptor, $ApiVersion + } + + # Try to invoke the REST method to create the group and return the result + try + { + $group = Invoke-AzDevOpsApiRestMethod @params + return $group + } + # Catch any exceptions and write an error message + catch + { + Write-Error "Failed to create group: $_" + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Remove-DevOpsGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Remove-DevOpsGroup.ps1 new file mode 100644 index 000000000..69e21d27f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Remove-DevOpsGroup.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS +Removes a group from Azure DevOps. + +.DESCRIPTION +The Remove-DevOpsGroup function is used to remove a group from Azure DevOps using the Azure DevOps REST API. + +.PARAMETER ApiUri +The mandatory parameter for the API URI. + +.PARAMETER ApiVersion +The optional parameter for the API version with a default value obtained from the Get-AzDevOpsApiVersion function. + +.PARAMETER GroupDescriptor +The optional parameter for the project scope descriptor. + +.OUTPUTS +System.Management.Automation.PSObject + +.EXAMPLE +Remove-DevOpsGroup -ApiUri "https://dev.azure.com/myorganization" -GroupDescriptor "MyGroup" + +This example removes the group with the specified group descriptor from Azure DevOps. + +#> +Function Remove-DevOpsGroup +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject])] + param + ( + [Parameter(Mandatory = $true)] + [string] + $ApiUri, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), + + [Parameter()] + [String] + $GroupDescriptor + ) + + $params = @{ + Uri = '{0}/_apis/graph/groups/{1}?api-version={2}' -f $ApiUri, $GroupDescriptor, $ApiVersion + Method = 'Delete' + ContentType = 'application/json' + } + + try + { + return (Invoke-AzDevOpsApiRestMethod @params) + } + catch + { + Write-Error "Failed to remove group: $_" + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Set-DevOpsGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Set-DevOpsGroup.ps1 new file mode 100644 index 000000000..0cbf79626 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Set-DevOpsGroup.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS +Updates an Azure DevOps group. + +.DESCRIPTION +The Set-DevOpsGroup function is used to update an Azure DevOps group by sending a PATCH request to the Azure DevOps REST API. + +.PARAMETER ApiUri +The mandatory parameter for the API URI. This should be the base URI of the Azure DevOps organization. + +.PARAMETER GroupName +The mandatory parameter for the group name. This specifies the name of the group to be updated. + +.PARAMETER GroupDescription +The optional parameter for the group description. This specifies the new description for the group. If not provided, the description will not be modified. + +.PARAMETER ApiVersion +The optional parameter for the API version. This specifies the version of the Azure DevOps REST API to use. If not provided, the default API version will be obtained from the Get-AzDevOpsApiVersion function. + +.PARAMETER ProjectScopeDescriptor +The optional parameter for the project scope descriptor. This specifies the scope descriptor for the project. If provided, the group will be updated within the specified project scope. + +.OUTPUTS +The function returns a PSObject representing the updated group. + +.EXAMPLE +Set-DevOpsGroup -ApiUri "https://dev.azure.com/contoso" -GroupName "MyGroup" -GroupDescription "Updated group description" + +This example updates the group named "MyGroup" in the Azure DevOps organization "https://dev.azure.com/contoso" with the new description "Updated group description". + +#> + +# This function is designed to update the description of a group in Azure DevOps. +Function Set-DevOpsGroup +{ + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([System.Management.Automation.PSObject])] + param + ( + # Parameter attribute marks this as a mandatory parameter that the user must supply when calling the function. + [Parameter(Mandatory = $true, ParameterSetName = 'ProjectScope')] + [Parameter(Mandatory = $true, ParameterSetName = 'Default')] + [string] + $ApiUri, # The URI for the Azure DevOps API. + + [Parameter(Mandatory = $true, ParameterSetName = 'ProjectScope')] + [Parameter(Mandatory = $true, ParameterSetName = 'Default')] + [string] + $GroupName, # The name of the group to be updated. + + # Optional parameter with a default value of $null if not specified by the user. + [Parameter(ParameterSetName = 'ProjectScope')] + [Parameter(ParameterSetName = 'Default')] + [String] + $GroupDescription = $null, # The new description for the group. + + # Optional parameter that gets the default API version if not specified. + [Parameter(ParameterSetName = 'ProjectScope')] + [Parameter(ParameterSetName = 'Default')] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), # The API version to use for the request. + + # Group Descriptor for the project within which the group exists. + [Parameter(Mandatory = $true, ParameterSetName = 'Default')] + [String] + $GroupDescriptor, + + # Optional parameter without a default value. + [Parameter(Mandatory = $true, ParameterSetName = 'ProjectScope')] + [String] + $ProjectScopeDescriptor # Scope descriptor for the project within which the group exists. + ) + + # A hashtable is created to hold parameters that will be used in the REST method invocation. + $params = @{ + Uri = '{0}/_apis/graph/groups/{1}?api-version={2}' -f $ApiUri, $GroupDescriptor, $ApiVersion # The API endpoint, formatted with the base URI and API version. + Method = 'Patch' # The HTTP method used for the request, indicating an update operation. + ContentType = 'application/json-patch+json' # The content type of the request body. + Body = @( + @{ + op = "replace" # Operation type in JSON Patch format, here adding a new value. + path = "/displayName" # The path in the target object to add the new value. + value = $GroupName # The value to add, which is the new group display name. + } + @{ + op = "replace" # Operation type in JSON Patch format, here adding a new value. + path = "/description" # The path in the target object to add the new value. + value = $GroupDescription # The value to add, which is the new group description. + } + ) | ConvertTo-Json # Convert the hashtable to JSON format for the request body. + } + + # If ProjectScopeDescriptor is provided, modify the URI to include it in the query parameters. + if ($ProjectScopeDescriptor) + { + $params.Uri = '{0}/_apis/graph/groups?scopeDescriptor={1}&api-version={2}' -f $ApiUri, $ProjectScopeDescriptor, $ApiVersion + } + + try + { + # Invoke the REST method with the parameters and store the result in $group. + $group = Invoke-AzDevOpsApiRestMethod @params + return $group # Return the result of the REST method call. + } + catch + { + # Write an error message to the console if the REST method call fails. + Write-Error "Failed to create group: $_" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/New-DevOpsGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/New-DevOpsGroupMember.ps1 new file mode 100644 index 000000000..061bec45d --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/New-DevOpsGroupMember.ps1 @@ -0,0 +1,94 @@ +<# +.SYNOPSIS + Adds a member to an Azure DevOps group using the Azure DevOps REST API. + +.DESCRIPTION + The New-DevOpsGroupMember function adds a specified member to a specified Azure DevOps group by invoking the Azure DevOps REST API. + It constructs the appropriate URI for the API call and uses the 'PUT' method to add the member to the group. + +.PARAMETER GroupIdentity + The identity of the group to which the member will be added. This parameter is mandatory. + +.PARAMETER MemberIdentity + The identity of the member to be added to the group. This parameter is mandatory. + +.PARAMETER ApiVersion + The version of the Azure DevOps API to use. If not specified, the default value is obtained from the Get-AzDevOpsApiVersion function. + +.PARAMETER ApiUri + The URI for the Azure DevOps API. This parameter is mandatory. + +.EXAMPLE + $group = Get-DevOpsGroup -Name "Developers" + $member = Get-DevOpsUser -UserName "jdoe" + New-DevOpsGroupMember -GroupIdentity $group -MemberIdentity $member -ApiUri "https://dev.azure.com/yourorganization" + + This example adds the user "jdoe" to the "Developers" group in the specified Azure DevOps organization. + +.NOTES + The function uses the Invoke-AzDevOpsApiRestMethod function to perform the REST API call. + It handles exceptions by writing an error message to the console if the API call fails. +#> +Function New-DevOpsGroupMember +{ + [CmdletBinding()] + param + ( + # The group Identity + [Parameter(Mandatory = $true)] + [Alias('Group')] + [Object]$GroupIdentity, + + # The group member + [Parameter(Mandatory = $true)] + [Alias('Member')] + [Object]$MemberIdentity, + + # Optional parameter for the API version with a default value obtained from the Get-AzDevOpsApiVersion function + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), + + # The URI for the Azure DevOps API. + [Parameter(Mandatory = $true)] + [string] + $ApiUri + ) + + # Define a hashtable to store parameters for the Invoke-AzDevOpsApiRestMethod function. + $params = @{ + # Construct the Uri using string formatting with the -f operator. + # It includes the API endpoint, group identity, member identity, and the API version. + Uri = '{0}/_apis/graph/memberships/{1}/{2}?api-version={3}' -f $ApiUri, + $MemberIdentity.descriptor, + $GroupIdentity.descriptor, + $ApiVersion + # Specifies the HTTP method to be used in the REST call, in this case 'PUT'. + Method = 'PUT' + } + + Write-Verbose "[Add-DevOpsGroupMember] Constructed URI for REST call: $($params.Uri)" + + # Try to invoke the REST method to create the group and return the result + + try + { + # Call the Invoke-AzDevOpsApiRestMethod function with the parameters defined above. + # The "@" symbol is used to pass the hashtable as splatting parameters. + Write-Verbose "[Add-DevOpsGroupMember] Attempting to invoke REST method to add group member." + $member = Invoke-AzDevOpsApiRestMethod @params + Write-Verbose "[Add-DevOpsGroupMember] Member added successfully." + } + catch + { + # If an exception occurs, write an error message to the console with details about the issue. + Write-Error "[Add-DevOpsGroupMember] Failed to add member to group: $($_.Exception.Message)" + } + + Write-Verbose "[Add-DevOpsGroupMember] Result $($member | ConvertTo-Json)." + + # Return the result of the REST method invocation, which is stored in $member. + Write-Verbose "[Add-DevOpsGroupMember] Returning result from REST method invocation." + return $member + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/Remove-DevOpsGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/Remove-DevOpsGroupMember.ps1 new file mode 100644 index 000000000..52d91fa15 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/Remove-DevOpsGroupMember.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS +Removes a member from an Azure DevOps group. + +.DESCRIPTION +The Remove-DevOpsGroupMember function removes a specified member from a specified Azure DevOps group using the Azure DevOps REST API. + +.PARAMETER GroupIdentity +The identity of the group from which the member will be removed. This parameter is mandatory. + +.PARAMETER MemberIdentity +The identity of the member to be removed from the group. This parameter is mandatory. + +.PARAMETER ApiVersion +The version of the Azure DevOps API to use. If not specified, the default version is obtained from the Get-AzDevOpsApiVersion function. + +.PARAMETER ApiUri +The base URI for the Azure DevOps API. This parameter is mandatory. + +.EXAMPLE +Remove-DevOpsGroupMember -GroupIdentity $group -MemberIdentity $member -ApiUri "https://dev.azure.com/organization" + +This example removes the specified member from the specified group in the Azure DevOps organization. + +.NOTES +This function constructs the appropriate URI for the Azure DevOps REST API call and uses the Invoke-AzDevOpsApiRestMethod function to perform the removal operation. If the operation fails, an error message is written to the console. +#> +Function Remove-DevOpsGroupMember +{ + [CmdletBinding()] + param + ( + # The group Identity + [Parameter(Mandatory = $true)] + [Alias('Group')] + [Object]$GroupIdentity, + + # The group member + [Parameter(Mandatory = $true)] + [Alias('Member')] + [Object]$MemberIdentity, + + # Optional parameter for the API version with a default value obtained from the Get-AzDevOpsApiVersion function + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), + + # The URI for the Azure DevOps API. + [Parameter(Mandatory = $true)] + [string] + $ApiUri + ) + + # Define a hashtable to store parameters for the Invoke-AzDevOpsApiRestMethod function. + $params = @{ + # Construct the Uri using string formatting with the -f operator. + # It includes the API endpoint, group identity, member identity, and the API version. + Uri = '{0}/_apis/graph/memberships/{1}/{2}?api-version={3}' -f $ApiUri, + $MemberIdentity.descriptor, + $GroupIdentity.descriptor, + $ApiVersion + # Specifies the HTTP method to be used in the REST call, in this case 'PUT'. + Method = 'DELETE' + } + + Write-Verbose "[Remove-DevOpsGroupMember] Constructed URI for REST call: $($params.Uri)" + + # Try to invoke the REST method to create the group and return the result + + try + { + # Call the Invoke-AzDevOpsApiRestMethod function with the parameters defined above. + # The "@" symbol is used to pass the hashtable as splatting parameters. + Write-Verbose "[Remove-DevOpsGroupMember] Attempting to invoke REST method to remove group member." + $member = Invoke-AzDevOpsApiRestMethod @params + Write-Verbose "[Remove-DevOpsGroupMember] Member removed successfully." + + } + catch + { + # If an exception occurs, write an error message to the console with details about the issue. + Write-Error "[Remove-DevOpsGroupMember] Failed to add member to group: $($_.Exception.Message)" + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.Remove-AzDevOpsApiResource.Tests.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Permission/Empty.txt similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.Remove-AzDevOpsApiResource.Tests.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Permission/Empty.txt diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/New-DevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/New-DevOpsProject.ps1 new file mode 100644 index 000000000..63c0e52c4 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/New-DevOpsProject.ps1 @@ -0,0 +1,102 @@ +<# +.SYNOPSIS +Creates a new Azure DevOps project. + +.DESCRIPTION +This function creates a new Azure DevOps project using the Azure DevOps REST API. It requires the organization name, project name, description, visibility (either "private" or "public"), and a personal access token for authentication. + +.PARAMETER Organization +The name of the Azure DevOps organization. + +.PARAMETER ProjectName +The name of the project to be created. + +.PARAMETER Description +A brief description of the project. + +.PARAMETER Visibility +The visibility of the project. Valid values are "private" or "public". + +.PARAMETER PersonalAccessToken +The personal access token used for authentication. + +.EXAMPLE +New-DevOpsProject -Organization "myorg" -ProjectName "MyProject" -Description "This is a new project" -Visibility "private" -PersonalAccessToken "mytoken" + +This example creates a new private Azure DevOps project named "MyProject" with the description "This is a new project" in the organization "myorg" using the specified personal access token. + +#> +function New-DevOpsProject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Organization, + + [Parameter()] + [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid -AllowWildcard })] + [Alias('Name')] + [System.String] + $ProjectName, + + [Parameter()] + [Alias('Description')] + [System.String] + $ProjectDescription, + + [Parameter()] + [System.String] + $SourceControlType, + + [Parameter()] + [System.String]$ProcessTemplateId, + + [Parameter()] + [System.String]$Visibility, + + # Get the latest API version. 7.1 is not supported by the API endpoint. + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion | Select-Object -Last 1) + ) + + # Validate the parameters + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/projects?api-version={1}' -f $Organization, $ApiVersion + Method = "POST" + Body = @{ + name = $ProjectName + description = $ProjectDescription + visibility = $Visibility + capabilities = @{ + versioncontrol = @{ + sourceControlType = $SourceControlType + } + processTemplate = @{ + templateTypeId = $ProcessTemplateId + } + } + } + } + + # Seralize the Body to JSON + $params.Body = $params.Body | ConvertTo-Json + + try + { + # Invoke the Azure DevOps REST API to create the project + $response = Invoke-AzDevOpsApiRestMethod @params + + if ($null -eq $response) + { + Throw "[New-DevOpsProject] Failed to create the Azure DevOps project: No response returned" + } + + # Output the response which contains the created project details + return $response + } + catch + { + Write-Error "[New-DevOpsProject] Failed to create the Azure DevOps project: $_" + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Remove-DevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Remove-DevOpsProject.ps1 new file mode 100644 index 000000000..e62d166ca --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Remove-DevOpsProject.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS +Removes an Azure DevOps project. + +.DESCRIPTION +The Remove-DevOpsProject function is used to remove an Azure DevOps project from the specified organization. + +.PARAMETER Organization +The name or URL of the Azure DevOps organization. + +.PARAMETER ProjectId +The ID or name of the project to be removed. + +.EXAMPLE +Remove-DevOpsProject -Organization "MyOrganization" -ProjectId "MyProject" + +This example removes the Azure DevOps project with the ID "MyProject" from the organization "MyOrganization". + +#> + +function Remove-DevOpsProject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Organization, + + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String] + $ProjectId, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion | Select-Object -Last 1) + + ) + + Write-Verbose "[Remove-DevOpsProject] Started." + + # Define the API version to use + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/projects/{1}?api-version={2}' -f $Organization, $ProjectId, $ApiVersion + Method = "DELETE" + } + + Write-Verbose "[Remove-DevOpsProject] Removing project $ProjectId from Azure DevOps organization $Organization" + + try + { + # Invoke the Azure DevOps REST API to create the project + $response = Invoke-AzDevOpsApiRestMethod @params + # Output the response which contains the created project details + return $response + } + catch + { + Write-Error "[Remove-DevOpsProject] Failed to create the Azure DevOps project: $_" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Update-DevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Update-DevOpsProject.ps1 new file mode 100644 index 000000000..a0fda13cc --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Update-DevOpsProject.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS +Updates an Azure DevOps project. + +.DESCRIPTION +This function updates an Azure DevOps project with the specified parameters. It allows you to change the project name, description, visibility, and personal access token. + +.PARAMETER Organization +The name or ID of the Azure DevOps organization. + +.PARAMETER ProjectId +The ID or name of the project to update. + +.PARAMETER NewName +The new name for the project. + +.PARAMETER Description +The new description for the project. + +.PARAMETER Visibility +The visibility of the project. Valid values are 'private' and 'public'. + +.PARAMETER PersonalAccessToken +The personal access token (PAT) used for authentication. + +.EXAMPLE +Update-DevOpsProject -Organization "contoso" -ProjectId "MyProject" -NewName "NewProjectName" -Description "Updated project description" -Visibility "public" -PersonalAccessToken "PAT" + +This example updates the project named "MyProject" in the "contoso" organization. It changes the project name to "NewProjectName", updates the description, sets the visibility to "public", and uses the specified personal access token for authentication. + +#> +function Update-DevOpsProject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Organization, + + [Parameter()] + [Alias('Name')] + [System.String] + $ProjectId, + + [Parameter()] + [Alias('Description')] + [System.String] + $ProjectDescription, + + [Parameter()] + [System.String]$ProcessTemplateId, + + [Parameter()] + [System.String]$Visibility, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion | Select-Object -Last 1) + + ) + + Write-Verbose "[Update-DevOpsProject] Updating project '$ProjectId' in organization '$Organization'" + + # Construct the body of the request + $body = @{ + name = $ProjectName + visibility = $Visibility + } + + # Add the description if provided + if ($ProjectDescription) + { + $body.description = $ProjectDescription + } + + # Construct the Paramters for the Invoke-AzDevOpsApiRestMethod function + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/projects/{1}?api-version={2}' -f $Organization, $ProjectId, $ApiVersion + Body = $body | ConvertTo-Json + Method = 'PATCH' + } + + # Invoke the Azure DevOps REST API to update the project + try + { + $response = Invoke-AzDevOpsApiRestMethod @params + } + catch + { + Write-Error "Failed to update the Azure DevOps project: $_" + } + + # Output the response which contains the updated project details + return $response + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Wait-DevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Wait-DevOpsProject.ps1 new file mode 100644 index 000000000..fcc173295 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Wait-DevOpsProject.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + Waits for a project to be created in Azure DevOps. + +.DESCRIPTION + The Wait-DevOpsProject function waits for a project to be created in Azure DevOps. It checks the status of the project creation and waits until the project is either created successfully or fails to be created. + +.PARAMETER OrganizationName + The name of the Azure DevOps organization. + +.PARAMETER ProjectURL + The URL of the project to wait for. + +.PARAMETER ApiVersion + The version of the Azure DevOps API to use. If not specified, the default API version will be used. + +.EXAMPLE + Wait-DevOpsProject -OrganizationName "MyOrg" -ProjectURL "https://dev.azure.com/MyOrg/MyProject" + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 +#> + +Function Wait-DevOpsProject +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter(Mandatory = $true)] + [string]$ProjectURL, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = '{0}' -f $ProjectURL + Method = "GET" + } + + Write-Verbose "[Wait-DevOpsProject] URI: $($params.URI)" + + # Loop until the project is created + $counter = 0 + do + { + Write-Verbose "[Wait-DevOpsProject] Sending request to check project status..." + $response = Invoke-AzDevOpsApiRestMethod @params + $project = $response + + # Check the status of the project + switch ($response.status) + { + 'creating' { + Write-Verbose "[Wait-DevOpsProject] Project is still being created..." + Start-Sleep -Seconds 5 + } + 'wellFormed' { + Write-Verbose "[Wait-DevOpsProject] Project has been created successfully." + break + } + 'failed' { + Write-Error "[Wait-DevOpsProject] Project creation failed: $response" + break + } + 'notSet' { + Write-Error "[Wait-DevOpsProject] Project creation status is not set: $response" + break + } + default { + # Still creating + Write-Verbose "[Wait-DevOpsProject] Project is still being created (default case)..." + Start-Sleep -Seconds 5 + } + } + + # Increment the counter + $counter++ + + } while ($counter -lt 10) + + if ($counter -ge 10) + { + Write-Error "[Wait-DevOpsProject] Timed out waiting for project to be created." + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Get-ProjectServiceStatus.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Get-ProjectServiceStatus.ps1 new file mode 100644 index 000000000..cb4e53d65 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Get-ProjectServiceStatus.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS +Retrieves the status of a specified project service in Azure DevOps. + +.DESCRIPTION +The Get-ProjectServiceStatus function retrieves the status of a specified service within a project in Azure DevOps. +It constructs the appropriate URI and makes a REST API call to fetch the service status. If the service status is +'undefined', it is treated as 'enabled'. + +.PARAMETER Organization +The name of the Azure DevOps organization. + +.PARAMETER ProjectId +The ID of the project in Azure DevOps. + +.PARAMETER ServiceName +The name of the service whose status is to be retrieved. + +.PARAMETER ApiVersion +The API version to use for the request. If not specified, the default API version is used. + +.OUTPUTS +System.Object +Returns the state of the specified service. + +.EXAMPLE +PS> Get-ProjectServiceStatus -Organization "MyOrg" -ProjectId "12345" -ServiceName "MyService" +This command retrieves the status of the service "MyService" in the project with ID "12345" within the organization "MyOrg". + +.NOTES +If the service status is 'undefined', it is treated as 'enabled'. +#> +function Get-ProjectServiceStatus +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Organization, + + [Parameter(Mandatory = $true)] + [string]$ProjectId, + + [Parameter(Mandatory = $true)] + [string]$ServiceName, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + # Get the project + # Construct the URI with optional state filter + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/FeatureManagement/FeatureStates/host/project/{1}/{2}?api-version={3}' -f $Organization, $ProjectId, $ServiceName, $ApiVersion + Method = 'Get' + } + + try + { + $response = Invoke-AzDevOpsApiRestMethod @params + # If the service is 'undefined' then treat it as 'enabled' + if ($response.state -eq 'undefined') + { + $response.state = 'enabled' + } + + # Output the state of the service + return $response + } + catch + { + Write-Error "Failed to get Security Descriptor: $_" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Set-ProjectServiceStatus.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Set-ProjectServiceStatus.ps1 new file mode 100644 index 000000000..691fdb430 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Set-ProjectServiceStatus.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS +Sets the status of a specified project service in Azure DevOps. + +.DESCRIPTION +The Set-ProjectServiceStatus function updates the status of a specified service within a given project in an Azure DevOps organization. It constructs the appropriate URI and sends a PATCH request with the provided body content. + +.PARAMETER Organization +The name of the Azure DevOps organization. + +.PARAMETER ProjectId +The ID of the project within the Azure DevOps organization. + +.PARAMETER ServiceName +The name of the service whose status is to be set. + +.PARAMETER Body +The body content to be sent in the PATCH request. This should be an object that will be converted to JSON. + +.PARAMETER ApiVersion +The API version to use for the request. If not specified, the default API version is retrieved using Get-AzDevOpsApiVersion. + +.EXAMPLE +Set-ProjectServiceStatus -Organization "myOrg" -ProjectId "12345" -ServiceName "myService" -Body $bodyContent + +.NOTES +This function requires the Azure DevOps REST API and the Invoke-AzDevOpsApiRestMethod cmdlet to be available. +#> +function Set-ProjectServiceStatus +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Organization, + + [Parameter(Mandatory = $true)] + [string]$ProjectId, + + [Parameter(Mandatory = $true)] + [string]$ServiceName, + + [Parameter(Mandatory = $true)] + [Object]$Body, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + # Get the project + # Construct the URI with optional state filter + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/FeatureManagement/FeatureStates/host/project/{1}/{2}?api-version={3}' -f $Organization, $ProjectId, $ServiceName, $ApiVersion + Method = 'PATCH' + Body = $Body | ConvertTo-Json + } + + try + { + $response = Invoke-AzDevOpsApiRestMethod @params + # Output the state of the service + return $response.state + } + catch + { + Write-Error "Failed to set Security Descriptor: $_" + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.Set-AzDevOpsApiResource.Tests.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Resource/Empty.txt similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/_TODO2.Set-AzDevOpsApiResource.Tests.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Resource/Empty.txt diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/_TODO2.New-AzDevOpsProject.Tests.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/RoleAssignments/Empty.txt similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/_TODO2.New-AzDevOpsProject.Tests.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/RoleAssignments/Empty.txt diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/_TODO2.Remove-AzDevOpsProject.Tests.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/RoleDefinition/empty.txt similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/_TODO2.Remove-AzDevOpsProject.Tests.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/RoleDefinition/empty.txt diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/SecurityDescriptor/Get-DevOpsSecurityDescriptor.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/SecurityDescriptor/Get-DevOpsSecurityDescriptor.ps1 new file mode 100644 index 000000000..faea69efa --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/SecurityDescriptor/Get-DevOpsSecurityDescriptor.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS +Retrieves the security descriptor for a project in Azure DevOps. + +.DESCRIPTION +The Get-DevOpsSecurityDescriptor function retrieves the security descriptor for a project in Azure DevOps. +It uses the Azure DevOps REST API to perform a lookup and retrieve the descriptor. + +.PARAMETER ProjectName +The name of the project. + +.PARAMETER Organization +The name of the Azure DevOps organization. + +.PARAMETER ApiVersion +The version of the Azure DevOps REST API to use. If not specified, the default version will be used. + +.EXAMPLE +Get-DevOpsSecurityDescriptor -ProjectId "ProjectID" -Organization "MyOrganization" + +This example retrieves the security descriptor for the project named "MyProject" in the Azure DevOps organization "MyOrganization". + +#> +function Get-DevOpsSecurityDescriptor +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $ProjectId, + [Parameter(Mandatory = $true)] + [string] + $Organization, + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + # Get the project + # Construct the URI with optional state filter + $params = @{ + Uri = 'https://vssps.dev.azure.com/{0}/_apis/graph/descriptors/{1}?api-version={2}' -f $Organization, $ProjectId, $ApiVersion + Method = 'Get' + } + + try + { + $response = Invoke-AzDevOpsApiRestMethod @params + # Output the security descriptor + return $response.value + } + catch + { + Write-Error "Failed to get Security Descriptor: $_" + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/_TODO2.Set-AzDevOpsProject.Tests.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ServiceEndpoint/empty.txt similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/_TODO2.Set-AzDevOpsProject.Tests.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ServiceEndpoint/empty.txt diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ServicePrincipal/empty.txt b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ServicePrincipal/empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Add-AuthenticationHTTPHeader.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Add-AuthenticationHTTPHeader.ps1 new file mode 100644 index 000000000..c740e94f3 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Add-AuthenticationHTTPHeader.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS +Adds the appropriate authentication HTTP header based on the type of authentication token. + +.DESCRIPTION +The Add-AuthenticationHTTPHeader function determines the type of authentication token and adds the corresponding HTTP header. +It supports Personal Access Tokens and Managed Identity Tokens. If the token is null or the token type is not supported, an error is thrown. + +.PARAMETER None +This function does not take any parameters. + +.OUTPUTS +String +Returns the authentication HTTP header as a string. + +.NOTES +The function relies on the global variables $Global:DSCAZDO_AuthenticationToken and $Global:DSCAZDO_OrganizationName. + +.EXAMPLE +$header = Add-AuthenticationHTTPHeader +# Adds the appropriate authentication HTTP header and returns it as a string. +#> + +Function Add-AuthenticationHTTPHeader +{ + # Dertimine the type of token. + $headerValue = "" + switch ($Global:DSCAZDO_AuthenticationToken.tokenType) + { + + # If the token is null + {[String]::IsNullOrEmpty($_)} { + throw "[Add-AuthenticationHTTPHeader] Error. The authentication token is null. Please ensure that the authentication token is set." + } + {$_ -eq 'PersonalAccessToken'} { + # Personal Access Token + + # Add the Personal Access Token to the header + $headerValue = 'Authorization: Basic {0}' -f $Global:DSCAZDO_AuthenticationToken.Get() + break + } + {$_ -eq 'ManagedIdentity'} { + # Managed Identity Token + Write-Verbose "[Add-AuthenticationHTTPHeader] Adding Managed Identity Token to the HTTP Headers." + + # Test if the Managed Identity Token has expired + if ($Global:DSCAZDO_AuthenticationToken.isExpired()) + { + Write-Verbose "[Add-AuthenticationHTTPHeader] Managed Identity Token has expired. Obtaining a new token." + # If so, get a new token + $Global:DSCAZDO_AuthenticationToken = Update-AzManagedIdentity -OrganizationName $Global:DSCAZDO_OrganizationName + } + + # Add the Managed Identity Token to the header + $headerValue = 'Bearer {0}' -f $Global:DSCAZDO_AuthenticationToken.Get() + break + + } + default { + throw "[Add-AuthenticationHTTPHeader] Error. The authentication token type is not supported." + } + + } + + Write-Verbose "[Add-AuthenticationHTTPHeader] Adding Header" + + # Return the header value + return $headerValue + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-AzManagedIdentityToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-AzManagedIdentityToken.ps1 new file mode 100644 index 000000000..09aa09324 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-AzManagedIdentityToken.ps1 @@ -0,0 +1,136 @@ +<# +.SYNOPSIS +Obtains a managed identity token from Azure AD. + +.DESCRIPTION +The Get-AzManagedIdentityToken function is used to obtain an access token from Azure AD using a managed identity. It can only be called from the New-AzDoAuthenticationProvider or Update-AzManagedIdentity functions. + +.PARAMETER OrganizationName +Specifies the name of the organization. + +.PARAMETER Verify +Specifies whether to verify the connection. If this switch is not set, the function returns the managed identity token. If the switch is set, the function tests the connection and returns the access token. + +.EXAMPLE +Get-AzManagedIdentityToken -OrganizationName "Contoso" -Verify +Obtains the access token for the managed identity associated with the organization "Contoso" and verifies the connection. + +.NOTES +This function does not require the Azure PowerShell module. +#> + +Function Get-AzManagedIdentityToken +{ + [CmdletBinding()] + param ( + # Organization Name + [Parameter(Mandatory = $true)] + [String] + $OrganizationName, + + # Verify the Connection + [Parameter()] + [Switch] + $Verify + ) + + Write-Verbose "[Get-AzManagedIdentityToken] Getting the managed identity token for the organization $OrganizationName." + + # Import the parameters + $ManagedIdentityParams = @{ + # Define the Azure instance metadata endpoint to get the access token + Uri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=499b84ac-1321-427f-aa17-267ca6975798" + Method = 'Get' + Headers = @{ Metadata="true" } + ContentType = 'Application/json' + NoAuthentication = $true + } + + # Dertimine if the machine is an arc machine + if ($env:IDENTITY_ENDPOINT) + { + + $OSInfo = Get-OperatingSystemInfo + + # Test if console is being run as Administrator + if ($OSInfo.Windows) + { + # Check if the current user is in the Administrator role + if (-not(Test-isWindowsAdmin)) { + throw "[Get-AzManagedIdentityToken] Error: Authentication to Azure Arc requires Administrator privileges." + } + } + + Write-Verbose "[Get-AzManagedIdentityToken] The machine is an Azure Arc machine. The Uri needs to be updated to $($env:IDENTITY_ENDPOINT):" + $ManagedIdentityParams.Uri = '{0}?api-version=2020-06-01&resource=499b84ac-1321-427f-aa17-267ca6975798' -f $env:IDENTITY_ENDPOINT + $ManagedIdentityParams.AzureArcAuthentication = $true + + } + else + { + Write-Verbose "[Get-AzManagedIdentityToken] The machine is not an Azure Arc machine. No changes are required." + } + + # Obtain the access token from Azure AD using the Managed Identity + Write-Verbose "[Get-AzManagedIdentityToken] Invoking the Azure Instance Metadata Service to get the access token." + + # Invoke the RestAPI + try + { + $response = Invoke-AzDevOpsApiRestMethod @ManagedIdentityParams + } + catch + { + # If there is an error it could be because it's an arc machine, and we need to use the secret file: + $wwwAuthHeader = $_.Exception.Response.Headers.WwwAuthenticate + if ($wwwAuthHeader -notmatch "Basic realm=.+") + { + Throw ('[Get-AzManagedIdentityToken] {0}' -f $_) + } + + Write-Verbose "[Get-AzManagedIdentityToken] Managed Identity Token Retrival Failed. Retrying with secret file." + + # Extract the secret file path from the WWW-Authenticate header + $secretFile = ($wwwAuthHeader -split "Basic realm=")[1] + # Read the secret file to get the token + $token = Get-Content -LiteralPath $secretFile -Raw + # Add the token to the headers + $ManagedIdentityParams.Headers.Authorization = "Basic $token" + + # Retry the request. Silently continue to suppress the error message, since we will handle it below. + $response = Invoke-AzDevOpsApiRestMethod @ManagedIdentityParams -ErrorAction SilentlyContinue + } + + # Test the response + if ($null -eq $response.access_token) + { + throw "Error. Access token not returned from Azure Instance Metadata Service. Please ensure that the Azure Instance Metadata Service is available." + } + + Write-Verbose "[Get-AzManagedIdentityToken] Managed Identity Token Retrival Successful." + + # TypeCast the response to a ManagedIdentityToken object + $ManagedIdentity = New-ManagedIdentityToken $response + # Null the response + $null = $response + + # Return the token if the verify switch is not set + if (-not($verify)) + { + return $ManagedIdentity + } + + Write-Verbose "[Get-AzManagedIdentityToken] Verifying the connection to the Azure DevOps API." + + # Test the Connection + if (-not(Test-AzToken $ManagedIdentity)) + { + throw "Error. Failed to call the Azure DevOps API." + } + + Write-Verbose "[Get-AzManagedIdentityToken] Connection Verified." + + # Return the AccessToken + return ($ManagedIdentity) + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-OperatingSystemInfo.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-OperatingSystemInfo.ps1 new file mode 100644 index 000000000..44d5263eb --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-OperatingSystemInfo.ps1 @@ -0,0 +1,38 @@ +Function Get-OperatingSystemInfo +{ + $OS = @{ + Windows = $( + if ($PSVersionTable.PSVersion.Major -le 5) + { + $true + } + else + { + $IsWindows + } + ) + Linux = $( + if ($null -eq $IsLinux) + { + $false + } + else + { + $IsLinux + } + ) + MacOS = $( + if ($null -eq $IsMacOS) + { + $false + } + else + { + $IsMacOS + } + ) + } + + Write-Output $OS + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Test-isWindowsAdmin.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Test-isWindowsAdmin.ps1 new file mode 100644 index 000000000..1c4fd7cab --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Test-isWindowsAdmin.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS + Checks if the current user has Administrator privileges. + +.DESCRIPTION + The Test-isWindowsAdmin function verifies if the current user is in the Administrator role. + If the user does not have Administrator privileges, an error is thrown indicating that + authentication to Azure Arc requires Administrator privileges. + +.EXAMPLE + Test-isWindowsAdmin + + This command checks if the current user has Administrator privileges. If not, an error is thrown. + +.NOTES + This function is used to ensure that the current user has the necessary permissions to + authenticate to Azure Arc. +#> + +Function Test-isWindowsAdmin +{ + + $currentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object System.Security.Principal.WindowsPrincipal($currentIdentity) + + # Check if the current user is in the Administrator role + ($principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Update-AzManagedIdentity.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Update-AzManagedIdentity.ps1 new file mode 100644 index 000000000..8a3cc2569 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Update-AzManagedIdentity.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS +Updates the Azure Managed Identity. + +.DESCRIPTION +This function updates the Azure Managed Identity by refreshing the token. + +.PARAMETER OrganizationName +The name of the organization associated with the Managed Identity. + +.EXAMPLE +Update-AzManagedIdentity -OrganizationName "Contoso" + +This example updates the Azure Managed Identity for the organization named "Contoso". + +#> + +Function Update-AzManagedIdentity +{ + # Test if the Global Var's Exist $Global:DSCAZDO_OrganizationName + if ($null -eq $Global:DSCAZDO_OrganizationName) + { + Throw "[Update-AzManagedIdentity] Organization Name is not set. Please run 'New-AzDoAuthenticationProvider -OrganizationName '" + } + + # Clear the existing token. + $Global:DSCAZDO_AuthenticationToken = $null + + # Refresh the Token. + $Global:DSCAZDO_AuthenticationToken = Get-AzManagedIdentityToken -OrganizationName $Global:DSCAZDO_OrganizationName +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/PersonalAccessToken/Set-AzPersonalAccessToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/PersonalAccessToken/Set-AzPersonalAccessToken.ps1 new file mode 100644 index 000000000..ae0c42902 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/PersonalAccessToken/Set-AzPersonalAccessToken.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS +Sets the Personal Access Token (PAT) for an Azure DevOps organization. + +.DESCRIPTION +The Set-AzPersonalAccessToken function sets the Personal Access Token (PAT) for an Azure DevOps organization. +It supports both plain text and secure string PATs. Optionally, it can verify the connection to the Azure DevOps API. + +.PARAMETER OrganizationName +Specifies the name of the Azure DevOps organization. + +.PARAMETER PersonalAccessToken +Specifies the Personal Access Token (PAT) in plain text. + +.PARAMETER SecureStringPersonalAccessToken +Specifies the Personal Access Token (PAT) as a secure string. + +.PARAMETER Verify +Indicates that the connection to the Azure DevOps API should be verified. + +.EXAMPLE +Set-AzPersonalAccessToken -OrganizationName "MyOrg" -PersonalAccessToken "myPAT" + +Sets the PAT for the organization "MyOrg" using the provided plain text PAT. + +.EXAMPLE +Set-AzPersonalAccessToken -OrganizationName "MyOrg" -SecureStringPersonalAccessToken $securePAT + +Sets the PAT for the organization "MyOrg" using the provided secure string PAT. + +.EXAMPLE +Set-AzPersonalAccessToken -OrganizationName "MyOrg" -PersonalAccessToken "myPAT" -Verify + +Sets the PAT for the organization "MyOrg" and verifies the connection to the Azure DevOps API. + +.NOTES +This function requires the New-PersonalAccessToken and Test-AzToken functions to be defined. +#> +Function Set-AzPersonalAccessToken +{ + [CmdletBinding(DefaultParameterSetName = 'PersonalAccessToken')] + param ( + # Organization Name + [Parameter(Mandatory = $true, ParameterSetName = 'PersonalAccessToken')] + [Parameter(Mandatory = $true, ParameterSetName = 'SecureStringPersonalAccessToken')] + [Alias('OrgName')] + [String] + $OrganizationName, + + # Personal Access Token + [Parameter(Mandatory = $true, ParameterSetName = 'PersonalAccessToken')] + [Alias("PAT")] + [String] + $PersonalAccessToken, + + # Secure String Personal Access Token + [Parameter(Mandatory = $true, ParameterSetName = 'SecureStringPersonalAccessToken')] + [Alias("SecureStringPAT")] + [SecureString] + $SecureStringPersonalAccessToken, + + # Verify the Connection + [Parameter(ParameterSetName = 'SecureStringPersonalAccessToken')] + [Parameter(ParameterSetName = 'PersonalAccessToken')] + [Switch] + $Verify + ) + + Write-Verbose "[Set-PersonalAccessToken] Setting the Personal Access Token for the organization $OrganizationName." + + # If a SecureString Personal Access Token is provided, parse it and set as the Token + if ($SecureStringPersonalAccessToken) + { + $Token = New-PersonalAccessToken -SecureStringPersonalAccessToken $SecureStringPersonalAccessToken + } + elseif ($PersonalAccessToken) + { + # TypeCast the response to a PersonalAccessToken object + $Token = New-PersonalAccessToken -PersonalAccessToken $PersonalAccessToken + } + else + { + throw "Error. A Personal Access Token or SecureString Personal Access Token must be provided." + } + + # + # Return the token if the verify switch is not set + if (-not($verify)) + { + return $Token + } + + Write-Verbose "[Set-PersonalAccessToken] Verifying the connection to the Azure DevOps API." + + # Test the Connection + if (-not(Test-AzToken $Token)) + { + throw "Error. Failed to call the Azure DevOps API." + } + + Write-Verbose "[Set-PersonalAccessToken] Connection Verified." + + # Return the AccessToken + return ($Token) + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Test-AzToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Test-AzToken.ps1 new file mode 100644 index 000000000..aa149ffe8 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Test-AzToken.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Tests the Azure Managed Identity token for accessing Azure DevOps REST API. + +.DESCRIPTION + The Test-AzToken function is used to test the Azure Managed Identity token for accessing the Azure DevOps REST API. + It calls the Azure DevOps REST API with the provided Managed Identity token and returns true if the token is valid, otherwise returns false. + +.PARAMETER Token + Specifies the Managed Identity token to be tested. + +.EXAMPLE + +#> + +Function Test-AzToken +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Object] + $Token + ) + + # Define the Azure DevOps REST API endpoint to get the list of projects + $AZDOProjectUrl = 'https://dev.azure.com/{0}/_apis/projects' -f $GLOBAL:DSCAZDO_OrganizationName + $FormattedUrl = '{0}?api-version=7.2-preview.4' -f $AZDOProjectUrl + + $params = @{ + Uri = $FormattedUrl + Method = 'Get' + Headers = @{ + Authorization = 'Bearer {0}' -f $Token.Get() + } + NoAuthentication = $true + } + + # Call the Azure DevOps REST API with the Managed Identity Bearer token + try + { + $null = Invoke-AzDevOpsApiRestMethod @params + } + catch + { + return $false + } + + return $true + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Add-CacheItem.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Add-CacheItem.ps1 new file mode 100644 index 000000000..dce8641aa --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Add-CacheItem.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Add a cache item to the cache. + +.DESCRIPTION +Adds a cache item to the cache with a specified key, value, and type. + +.PARAMETER Key +The key of the cache item to add. + +.PARAMETER Value +The value of the cache item to add. + +.PARAMETER Type +The type of the cache item to add. Valid values are 'Project', 'Team', 'Group', 'SecurityDescriptor'. + +.EXAMPLE +Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' + +.NOTES +This function is private and should not be used directly. +#> +Function Add-CacheItem +{ + [CmdletBinding()] + param ( + # The key of the cache item to add + [Parameter(Mandatory = $true)] + [string] + $Key, + + # The value of the cache item to add + [Parameter(Mandatory = $true)] + [object] + $Value, + + # The type of the cache item to add + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string] + $Type, + + # Suppress warning messages + [switch] + $SuppressWarning + ) + + Write-Verbose "[Add-CacheItem] Retrieving the current cache." + [System.Collections.Generic.List[CacheItem]]$cache = Get-CacheObject -CacheType $Type + + # If the cache is empty, create a new cache + if ($cache.count -eq 0) + { + Write-Verbose "[Add-CacheItem] Cache is empty. Creating new cache." + $cache = [System.Collections.Generic.List[CacheItem]]::New() + } + + Write-Verbose "[Add-CacheItem] Creating new cache item with key: '$Key'." + $cacheItem = [CacheItem]::New($Key, $Value) + + Write-Verbose "[Add-CacheItem] Checking if the cache already contains the key: '$Key'." + $existingItem = $cache | Where-Object { $_.Key -eq $Key } + + if ($existingItem) + { + # If the cache already contains the key, remove the existing item + if ($SuppressWarning.IsPresent) + { + Write-Verbose "[Add-CacheItem] A cache item with the key '$Key' already exists. Flushing key from the cache." + } + else + { + Write-Warning "[Add-CacheItem] A cache item with the key '$Key' already exists. Flushing key from the cache." + } + + # Remove the existing cache item + Remove-CacheItem -Key $Key -Type $Type + + # Refresh the cache + [System.Collections.Generic.List[CacheItem]]$cache = Get-CacheObject -CacheType $Type + + # If the cache is empty, create a new cache + if ($cache.count -eq 0) + { + Write-Verbose "[Add-CacheItem] Cache is empty. Creating new cache." + $cache = [System.Collections.Generic.List[CacheItem]]::New() + } + + } + + Write-Verbose "[Add-CacheItem] Adding new cache item with key: '$Key'." + $cache.Add($cacheItem) + + # Update the memory cache + Set-Variable -Name "AzDo$Type" -Value $cache -Scope Global + + Write-Verbose "[Add-CacheItem] Cache item with key: '$Key' successfully added." +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/0.ProjectCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/0.ProjectCache.ps1 new file mode 100644 index 000000000..1368700e9 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/0.ProjectCache.ps1 @@ -0,0 +1,94 @@ +<# +.SYNOPSIS +Sets the Azure DevOps API project cache. + +.DESCRIPTION +This function sets the Azure DevOps API project cache by making an API request to get the projects and adding them to the cache. + +.PARAMETER OrganizationName +Specifies the name of the organization. If not provided, the function uses a global variable as a fallback. + +.EXAMPLE +AzDoAPI_0_ProjectCache -OrganizationName "MyOrganization" +This example sets the Azure DevOps API project cache for the organization "MyOrganization". + +.INPUTS +None. + +.OUTPUTS +None. + +.NOTES +Author: Michael Zanatta +Date: 2025-01-06 +#> + +Function AzDoAPI_0_ProjectCache +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "Starting 'AzDoAPI_0_ProjectCache' function." + + if (-not $OrganizationName) + { + # If no organization name is provided, use a global variable as fallback + Write-Verbose "No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + # Create a hashtable to store parameters for API call + $params = @{ + Organization = $OrganizationName + } + + try + { + # Inform about the API call being made with the parameters + Write-Verbose "Calling 'List-DevOpsProjects' with parameters: $($params | Out-String)" + + # Perform an Azure DevOps API request to get the projects + $projects = List-DevOpsProjects @params + $projectsArr = [System.Collections.ArrayList]::new() + + # Iterate through each project and get the security descriptors + foreach ($project in $projects) + { + # Add the Project + $securityDescriptor = Get-DevOpsSecurityDescriptor -ProjectId $project.Id -Organization $OrganizationName + # Add the security descriptor to the project object + $projectsArr.Add(($project | Select-Object *, @{Name='ProjectDescriptor'; Expression={$securityDescriptor}})) + } + + # Log the total number of projects returned by the API call + Write-Verbose "'List-DevOpsProjects' returned a total of $($projects.Count) projects." + + # Iterate through each project in the response and add them to the cache + foreach ($project in $projectsArr) + { + # Log the addition of each project to the cache + Write-Verbose "Adding Project '$($project.Name)' to the cache." + # Add the project to the cache with its name as the key + Add-CacheItem -Key $project.Name -Value $project -Type 'LiveProjects' + } + + # Export the cache to a file + Export-CacheObject -CacheType 'LiveProjects' -Content $AzDoLiveProjects + + # Indicate completion of adding projects to cache + Write-Verbose "Completed adding projects to cache." + + } + catch + { + # Handle any exceptions that occur during the try block + Write-Error "An error occurred: $_" + } + + # Signal the end of the function execution + Write-Verbose "Function 'AzDoAPI_0_ProjectCache' completed." +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/1.GroupCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/1.GroupCache.ps1 new file mode 100644 index 000000000..a9ccc99b2 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/1.GroupCache.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS +Sets the Azure DevOps API group cache. + +.DESCRIPTION +This function sets the cache for Azure DevOps API groups. It retrieves the groups using the List-AzDevOpsGroup function and adds them to the cache. + +.PARAMETER OrganizationName +The name of the organization. If not provided as a parameter, it uses the global variable $Global:DSCAZDO_OrganizationName. + +.EXAMPLE +AzDoAPI_1_GroupCache -OrganizationName "MyOrganization" +#> +Function AzDoAPI_1_GroupCache +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "Starting 'Set-GroupCache' function." + + if (-not $OrganizationName) + { + Write-Verbose "No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + $params = @{ + Organization = $OrganizationName + } + + try + { + Write-Verbose "Calling 'List-DevOpsGroups' with parameters: $($params | Out-String)" + # Perform an Azure DevOps API request to get the groups + + $groups = List-DevOpsGroups @params + + Write-Verbose "'List-DevOpsGroups' returned a total of $($groups.Count) groups." + + # Iterate through each of the responses and add them to the cache + foreach ($group in $groups) + { + Write-Verbose "Adding group '$($group.PrincipalName)' to cache." + # Add the group to the cache + Add-CacheItem -Key $group.PrincipalName -Value $group -Type 'LiveGroups' + } + + # Export the cache to a file + Export-CacheObject -CacheType 'LiveGroups' -Content $AzDoLiveGroups + Write-Verbose "Completed adding groups to cache." + + } + catch + { + Write-Error "An error occurred: $_" + } + + Write-Verbose "Function 'AzDoAPI_1_GroupCache' completed." +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/2.UserCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/2.UserCache.ps1 new file mode 100644 index 000000000..3692471fd --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/2.UserCache.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS +Initializes and populates the user cache for Azure DevOps. + +.DESCRIPTION +The `AzDoAPI_2_UserCache` function initializes and populates the user cache by retrieving user information from Azure DevOps. +It uses the provided organization name or a global variable if no organization name is provided. The function retrieves the +user list, adds each user to the cache, and then exports the cache to a file. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. If not provided, the function will use the global variable `$Global:DSCAZDO_OrganizationName`. + +.EXAMPLE +PS> AzDoAPI_2_UserCache -OrganizationName "MyOrganization" +This example initializes and populates the user cache for the specified Azure DevOps organization "MyOrganization". + +.NOTES +- This function uses the `List-UserCache` cmdlet to retrieve the list of users. +- Each user is added to the cache using the `Add-CacheItem` cmdlet. +- The cache is exported to a file using the `Export-CacheObject` cmdlet. +- Verbose output is provided to indicate the progress and actions of the function. +#> +Function AzDoAPI_2_UserCache +{ + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "[AzDoAPI_2_UserCache] Starting 'AzDoAPI_2_UserCache' function." + + if (-not $OrganizationName) + { + Write-Verbose "[AzDoAPI_2_UserCache] No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + $params = @{ + Organization = $OrganizationName + } + + try + { + Write-Verbose "[AzDoAPI_2_UserCache] Calling 'AzDoAPI_2_UserCache' with parameters: $($params | Out-String)" + # Perform an Azure DevOps API request to get the groups + + $users = List-UserCache @params + + Write-Verbose "[AzDoAPI_2_UserCache] 'AzDoAPI_2_UserCache' returned a total of $($users.Count) users." + + # Iterate through each of the responses and add them to the cache + foreach ($user in $users) + { + Write-Verbose "[AzDoAPI_2_UserCache] Adding user '$($user.PrincipalName)' to cache." + # Add the group to the cache + Add-CacheItem -Key $user.PrincipalName -Value $user -Type 'LiveUsers' + } + + # Export the cache to a file + Export-CacheObject -CacheType 'LiveUsers' -Content $AzDoLiveUsers + + Write-Verbose "[AzDoAPI_2_UserCache] Completed adding users to cache." + + } + catch + { + Write-Error "[AzDoAPI_2_UserCache] An error occurred: $_" + } + + Write-Verbose "[AzDoAPI_2_UserCache] Function 'Set-AzDoAPICacheGroup' completed." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/3.GroupMemberCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/3.GroupMemberCache.ps1 new file mode 100644 index 000000000..c51ab6280 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/3.GroupMemberCache.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS +Initializes and updates the Azure DevOps group member cache. + +.DESCRIPTION +The `AzDoAPI_3_GroupMemberCache` function initializes and updates the cache for Azure DevOps group members. +It retrieves the live group and user caches, iterates through each group, and updates the cache with the members of each group. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. If not provided, the global variable `$Global:DSCAZDO_OrganizationName` is used. + +.EXAMPLE +AzDoAPI_3_GroupMemberCache -OrganizationName "MyOrganization" +Initializes and updates the group member cache for the specified Azure DevOps organization. + +.NOTES +- This function relies on the `Get-CacheObject`, `List-DevOpsGroupMembers`, and `Add-CacheItem` functions. +- The cache is exported to a file at the end of the function. +- Verbose output is used to indicate the progress of the function. + +#> +function AzDoAPI_3_GroupMemberCache +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "Starting 'Set-GroupCache' function." + + if (-not $OrganizationName) + { + Write-Verbose "No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + $params = @{ + Organization = $OrganizationName + } + + # Enumerate the live group cache + $AzDoLiveGroups = Get-CacheObject -CacheType 'LiveGroups' + # Enumerate the live users cache + $AzDoLiveUsers = Get-CacheObject -CacheType 'LiveUsers' + + try + { + ForEach ($AzDoLiveGroup in $AzDoLiveGroups) + { + # Update the Group ID in the parameters + $GroupDescriptor = $AzDoLiveGroup.Value.descriptor + + Write-Verbose "Calling 'AzDoAPI_2_GroupMemberCache' with parameters: $($params | Out-String)" + + # Perform an Azure DevOps API request to get the groups + $groupMembers = List-DevOpsGroupMembers -Organization $OrganizationName -GroupDescriptor $GroupDescriptor + + # If there are no members, skip to the next group + if ($null -eq $groupMembers.memberDescriptor) + { + Write-Verbose "No members found for group '$($AzDoLiveGroup.Key)'; skipping." + continue + } + + # Members + $members = [System.Collections.Generic.List[object]]::new() + + # Iterate through each of the users and groups and add them to the cache + $azdoUserMembers = $AzDoLiveUsers.value | Where-Object { $_.descriptor -in $groupMembers.memberDescriptor } + $azdoGroupMembers = $AzDoLiveGroups.value | Where-Object { $_.descriptor -in $groupMembers.memberDescriptor } + + # Add the users to the cache + $azdoUserMembers = $azdoUserMembers | Select-Object *,@{Name="Type";Exp={"user"}} + $azdoUserMembers | Where-Object { $_.descriptor -in $groupMembers.memberDescriptor } | ForEach-Object { + $null = $members.Add($_) + } + + # Add the groups to the cache + $azdoGroupMembers = $azdoGroupMembers | Select-Object *,@{Name="Type";Exp={"group"}} + $azdoGroupMembers | Where-Object { $_.descriptor -in $groupMembers.memberDescriptor } | ForEach-Object { + $null = $members.Add($_) + } + + # Add the group to the cache + Add-CacheItem -Key $AzDoLiveGroup.value.PrincipalName -Value $members -Type 'LiveGroupMembers' + + } + + # Export the cache to a file + Export-CacheObject -CacheType 'LiveGroupMembers' -Content $AzdoLiveGroupMembers + + Write-Verbose "Completed adding groups to cache." + + } + catch + { + Write-Error "An error occurred: $_" + } + + Write-Verbose "Function 'Set-AzDoAPICacheGroup' completed." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/4.GitRepositoryCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/4.GitRepositoryCache.ps1 new file mode 100644 index 000000000..5d601ded5 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/4.GitRepositoryCache.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS +Initializes the Git repository cache for Azure DevOps projects. + +.DESCRIPTION +The AzDoAPI_4_GitRepositoryCache function initializes the Git repository cache by enumerating live projects and retrieving their repositories from Azure DevOps. The repositories are then added to the cache. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. If not provided, the function uses the global variable $Global:DSCAZDO_OrganizationName. + +.EXAMPLE +AzDoAPI_4_GitRepositoryCache -OrganizationName "MyOrganization" +Initializes the Git repository cache for the specified Azure DevOps organization. + +.EXAMPLE +AzDoAPI_4_GitRepositoryCache +Initializes the Git repository cache using the global organization name variable. + +.NOTES +This function uses verbose logging to indicate the progress and actions taken during the cache initialization process. It also handles errors by logging them as errors. + +#> +function AzDoAPI_4_GitRepositoryCache +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "[AzDoAPI_4_GitRepositoryCache] Started." + + if (-not $OrganizationName) + { + Write-Verbose "[AzDoAPI_4_GitRepositoryCache] No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + # Enumerate the live projects cache + $AzDoLiveProjects = Get-CacheObject -CacheType 'LiveProjects' + + try + { + foreach ($AzDoLiveProject in $AzDoLiveProjects) + { + # Log the project being processed + Write-Verbose "[AzDoAPI_4_GitRepositoryCache] Processing Project '$($AzDoLiveProject.Value.Name)'." + $ProjectName = $AzDoLiveProject.Value.Name + + # Call the API to get the repositories for the project + $enumeratedRepositories = List-DevOpsGitRepository -ProjectName $ProjectName -OrganizationName $OrganizationName + + # Log the the git repositories returned by the API call + Write-Verbose "[AzDoAPI_4_GitRepositoryCache] 'List-DevOpsGitRepository' returned a total of $($enumeratedRepositories.Count) repositories." + + # Iterate through each repository in the response and add them to the cache + foreach ($repository in $enumeratedRepositories) + { + # Log the addition of each repository to the cache + Write-Verbose "[AzDoAPI_4_GitRepositoryCache] Adding Repository '$($repository.Name)' to the cache." + # Add the repository to the cache with its name as the key + Add-CacheItem -Key "$ProjectName\$($repository.Name)" -Value $repository -Type 'LiveRepositories' + } + + } + + # Export the cache to a file + Export-CacheObject -CacheType 'LiveRepositories' -Content $AzDoLiveRepositories + Write-Verbose "[AzDoAPI_4_GitRepositoryCache] Completed adding groups to cache." + + } + catch + { + Write-Error "[AzDoAPI_4_GitRepositoryCache] An error occurred: $_" + } + + Write-Verbose "[AzDoAPI_4_GitRepositoryCache] completed." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/5.PermissionsCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/5.PermissionsCache.ps1 new file mode 100644 index 000000000..63f2897d6 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/5.PermissionsCache.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS +Initializes and caches the permissions for Azure DevOps security namespaces. + +.DESCRIPTION +The `AzDoAPI_5_PermissionsCache` function retrieves the security namespaces for a specified Azure DevOps organization and caches the permissions. +If no organization name is provided, it uses a global variable for the organization name. The function then exports the cached permissions to a file. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. This parameter is optional. If not provided, the function uses the global variable `$Global:DSCAZDO_OrganizationName`. + +.EXAMPLE +AzDoAPI_5_PermissionsCache -OrganizationName "MyOrganization" +This example initializes and caches the permissions for the "MyOrganization" Azure DevOps organization. + +.EXAMPLE +AzDoAPI_5_PermissionsCache +This example initializes and caches the permissions using the global organization name variable. + +.NOTES +This function requires the `List-DevOpsSecurityNamespaces`, `Add-CacheItem`, and `Export-CacheObject` cmdlets to be available in the session. +#> +function AzDoAPI_5_PermissionsCache +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$OrganizationName + ) + + # + # Use a verbose statement to indicate the start of the function. + + Write-Verbose "[AzDoAPI_5_PermissionsCache] Started." + + if (-not $OrganizationName) + { + Write-Verbose "[AzDoAPI_5_PermissionsCache] No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + # + # List the security namespaces + + $securityNamespaces = List-DevOpsSecurityNamespaces -OrganizationName $OrganizationName + + # + # Iterate through each security namespace and export the permissions to the cache + foreach ($securityNamespace in $securityNamespaces) + { + $securityNamespaceName = $securityNamespace.name + $value = $securityNamespace | Select-Object namespaceId, name, displayName, writePermission, readPermision, dataspaceCategory, actions + + # Add the project to the cache with its name as the key + Add-CacheItem -Key $securityNamespaceName -Value $value -Type 'SecurityNamespaces' + } + + # Export the cache to a file + Export-CacheObject -CacheType 'SecurityNameSpaces' -Content $AzDoSecurityNameSpaces -Depth 5 + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/6.ServicePrinciple.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/6.ServicePrinciple.ps1 new file mode 100644 index 000000000..70241edb4 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/6.ServicePrinciple.ps1 @@ -0,0 +1,71 @@ +<# +.SYNOPSIS +Initializes the cache with Azure DevOps service principals. + +.DESCRIPTION +The AzDoAPI_6_ServicePrinciple function retrieves service principals from Azure DevOps and adds them to a cache. +If no organization name is provided, it uses a global variable for the organization name. +The function then exports the cache to a file. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. If not provided, the function uses the global variable $Global:DSCAZDO_OrganizationName. + +.EXAMPLE +AzDoAPI_6_ServicePrinciple -OrganizationName "MyOrganization" +This example initializes the cache with service principals from the "MyOrganization" Azure DevOps organization. + +.NOTES +This function uses the List-DevOpsServicePrinciples cmdlet to retrieve service principals and the Add-CacheItem cmdlet to add them to the cache. +The cache is then exported using the Export-CacheObject cmdlet. + +#> +function AzDoAPI_6_ServicePrinciple +{ + [CmdletBinding()] + param( + [string]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "Starting [AzDoAPI_6_ServicePrinciple] function." + + if (-not $OrganizationName) + { + Write-Verbose "No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + $params = @{ + Organization = $OrganizationName + } + + try + { + Write-Verbose "[AzDoAPI_6_ServicePrinciple] with parameters: $($params | Out-String)" + # Perform an Azure DevOps API request to get the groups + + $serviceprincipals = List-DevOpsServicePrinciples @params + + Write-Verbose "[AzDoAPI_6_ServicePrinciple] returned a total of $($serviceprincipals.Count) serviceprincipals." + + # Iterate through each of the responses and add them to the cache + foreach ($serviceprincipal in $serviceprincipals) + { + Write-Verbose "[AzDoAPI_6_ServicePrinciple] Adding serviceprincipal '$($serviceprincipal.displayName)' to cache." + # Add the group to the cache + Add-CacheItem -Key $serviceprincipal.DisplayName -Value $serviceprincipal -Type 'LiveServicePrinciples' + } + + # Export the cache to a file + Export-CacheObject -CacheType 'LiveServicePrinciples' -Content $AzDoLiveServicePrinciples + + Write-Verbose "[AzDoAPI_6_ServicePrinciple] Completed adding serviceprincipals to cache." + + } + catch + { + Write-Error "An error occurred: $_" + } + + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/7.IdentitySubjectDescriptors.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/7.IdentitySubjectDescriptors.ps1 new file mode 100644 index 000000000..6bdcccd80 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/7.IdentitySubjectDescriptors.ps1 @@ -0,0 +1,153 @@ +<# +.SYNOPSIS + Initializes and updates the identity subject descriptors cache for Azure DevOps groups, users, and service principals. + +.DESCRIPTION + The AzDoAPI_7_IdentitySubjectDescriptors function retrieves and updates the identity subject descriptors for Azure DevOps groups, users, and service principals. + It uses the provided organization name or a global variable if no organization name is provided. The function enumerates the live groups, users, and service principals + from the cache, queries their identities, and updates the cache with the retrieved identity information. + +.PARAMETER OrganizationName + The name of the Azure DevOps organization. If not provided, the function uses the global variable $Global:DSCAZDO_OrganizationName. + +.EXAMPLE + PS> AzDoAPI_7_IdentitySubjectDescriptors -OrganizationName "MyOrganization" + Initializes and updates the identity subject descriptors cache for the specified Azure DevOps organization. + +.EXAMPLE + PS> AzDoAPI_7_IdentitySubjectDescriptors + Initializes and updates the identity subject descriptors cache using the global organization name. + +.NOTES + This function is part of the AzureDevOpsDsc module and is used internally to manage the identity subject descriptors cache. +#> +function AzDoAPI_7_IdentitySubjectDescriptors +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$OrganizationName + ) + + # + # Use a verbose statement to indicate the start of the function. + + Write-Verbose "[AzDoAPI_5_PermissionsCache] Started." + + if (-not $OrganizationName) + { + Write-Verbose "[AzDoAPI_5_PermissionsCache] No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + # Enumerate the live group cache + $AzDoLiveGroups = Get-CacheObject -CacheType 'LiveGroups' + # Enumerate the live users cache + $AzDoLiveUsers = Get-CacheObject -CacheType 'LiveUsers' + # Enumerate the live service principals cache + $AzDoLiveServicePrinciples = Get-CacheObject -CacheType 'LiveServicePrinciples' + + # + # Iterate through each of the groups and query the Identity and add to the cache + + $params = @{ + OrganizationName = $OrganizationName + } + + # Iterate through each of the groups and query the Identity and add to the cache + ForEach ($AzDoLiveGroup in $AzDoLiveGroups) + { + $identity = Get-DevOpsDescriptorIdentity @params -SubjectDescriptor $AzDoLiveGroup.value.descriptor + $ACLIdentity = [PSCustomObject]@{ + id = $identity.id + descriptor = $identity.descriptor + subjectDescriptor = $identity.subjectDescriptor + providerDisplayName = $identity.providerDisplayName + isActive = $identity.isActive + isContainer = $identity.isContainer + } + + $AzDoLiveGroup.value | Add-Member -MemberType NoteProperty -Name 'ACLIdentity' -Value $ACLIdentity + + $cacheParams = @{ + Key = $AzDoLiveGroup.Key + Value = $AzDoLiveGroup + Type = 'LiveGroups' + SuppressWarning = $true + } + + # Add to the cache + Add-CacheItem @cacheParams + + } + + # Update the cache + Export-CacheObject -CacheType 'LiveGroups' -Content $AzDoLiveGroups + + # + # Iterate through each of the users and query the Identity and add to the cache + + ForEach ($AzDoLiveUser in $AzDoLiveUsers) + { + $identity = Get-DevOpsDescriptorIdentity @params -SubjectDescriptor $AzDoLiveUser.value.descriptor + + $ACLIdentity = [PSCustomObject]@{ + id = $identity.id + descriptor = $identity.descriptor + subjectDescriptor = $identity.subjectDescriptor + providerDisplayName = $identity.providerDisplayName + isActive = $identity.isActive + isContainer = $identity.isContainer + } + + $AzDoLiveUser.value | Add-Member -MemberType NoteProperty -Name 'ACLIdentity' -Value $ACLIdentity + + $cacheParams = @{ + Key = $AzDoLiveUser.Key + Value = $AzDoLiveUser + Type = 'LiveUsers' + SuppressWarning = $true + } + + # Add to the cache + Add-CacheItem @cacheParams + + } + + # Update the cache + Export-CacheObject -CacheType 'LiveUsers' -Content $AzDoLiveUsers + + # + # Iterate through each of the service principals and query the Identity and add to the cache + + ForEach ($AzDoLiveServicePrinciple in $AzDoLiveServicePrinciples) + { + $identity = Get-DevOpsDescriptorIdentity @params -SubjectDescriptor $AzDoLiveServicePrinciple.value.descriptor + + $ACLIdentity = [PSCustomObject]@{ + id = $identity.id + descriptor = $identity.descriptor + subjectDescriptor = $identity.subjectDescriptor + providerDisplayName = $identity.providerDisplayName + isActive = $identity.isActive + isContainer = $identity.isContainer + } + + $AzDoLiveServicePrinciple.value | Add-Member -MemberType NoteProperty -Name 'ACLIdentity' -Value $ACLIdentity + + $cacheParams = @{ + Key = $AzDoLiveServicePrinciple.Key + Value = $AzDoLiveServicePrinciple + Type = 'LiveServicePrinciples' + SuppressWarning = $true + } + + # Add to the cache + Add-CacheItem @cacheParams + + } + + # Update the cache + Export-CacheObject -CacheType 'LiveServicePrinciples' -Content $AzDoLiveServicePrinciples + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/8.ProcessTemplates.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/8.ProcessTemplates.ps1 new file mode 100644 index 000000000..e3d88976f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/8.ProcessTemplates.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS +Retrieves and caches Azure DevOps project process templates for a specified organization. + +.DESCRIPTION +The 'AzDoAPI_8_ProjectProcessTemplates' function retrieves the list of project process templates +from Azure DevOps for a specified organization and caches them. If no organization name is provided +as a parameter, it uses a global variable for the organization name. The function then exports the +cached process templates to a file. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization for which to retrieve the project process templates. + +.EXAMPLE +PS> AzDoAPI_8_ProjectProcessTemplates -OrganizationName "MyOrganization" + +This example retrieves and caches the project process templates for the organization named "MyOrganization". + +.NOTES +This function uses the 'List-DevOpsProcess' cmdlet to retrieve the process templates and the 'Add-CacheItem' +cmdlet to add each process template to the cache. The cache is then exported using the 'Export-CacheObject' cmdlet. + +#> +function AzDoAPI_8_ProjectProcessTemplates +{ + [CmdletBinding()] + param( + [string]$OrganizationName + ) + + # Use a verbose statement to indicate the start of the function. + Write-Verbose "[AzDoAPI_8_ProjectProcessTemplates] Starting 'AzDoAPI_8_ProjectProcessTemplates' function." + + if (-not $OrganizationName) + { + Write-Verbose "[AzDoAPI_8_ProjectProcessTemplates] No organization name provided as parameter; using global variable." + $OrganizationName = $Global:DSCAZDO_OrganizationName + } + + # Construct the parameters for the API request + $params = @{ + Organization = $OrganizationName + } + + try + { + Write-Verbose "[AzDoAPI_8_ProjectProcessTemplates] Calling 'List-DevOpsProcess' with parameters: $($params | Out-String)" + # Perform an Azure DevOps API request to get the groups + + $processes = List-DevOpsProcess @params + + # Iterate through each of the responses and add them to the cache + foreach ($process in $processes) + { + Write-Verbose "[AzDoAPI_8_ProjectProcessTemplates] Adding process '$($process.name)' to cache." + # Add the group to the cache + Add-CacheItem -Key $process.name -Value $process -Type 'LiveProcesses' + } + + # Export the cache to a file + Export-CacheObject -CacheType 'LiveProcesses' -Content $AzDoLiveProcesses + + Write-Verbose "[AzDoAPI_8_ProjectProcessTemplates] Completed adding processes to cache." + + } + catch + { + Write-Error "[AzDoAPI_8_ProjectProcessTemplates] An error occurred: $_" + } + + Write-Verbose "[AzDoAPI_8_ProjectProcessTemplates] Function completed." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Export-CacheObject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Export-CacheObject.ps1 new file mode 100644 index 000000000..72eda4ce8 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Export-CacheObject.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS +Exports content to a cache file and saves it to a global variable. + +.DESCRIPTION +The Export-CacheObject function exports content to a cache file and saves it to a global variable. It is used in the AzureDevOpsDsc module for caching Azure DevOps API responses. + +.PARAMETER CacheType +Specifies the type of cache. Valid values are 'Project', 'Team', 'Group', and 'SecurityDescriptor'. + +.PARAMETER Content +Specifies the content to be exported to the cache file. + +.PARAMETER Depth +Specifies the depth of the object to be exported. Default value is 3. + +.PARAMETER CacheRootPath +Specifies the root path where the cache directory will be created. Default value is the script root path. + +.EXAMPLE +Export-CacheObject -CacheType 'Project' -Content $projectData +Exports the $projectData content to a cache file and saves it to the global variable 'AzDoProject'. + +.INPUTS +None. + +.OUTPUTS +None. + +.NOTES +This function is part of the AzureDevOpsDsc module and is used for caching Azure DevOps API responses. +#> +Function Export-CacheObject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string]$CacheType, + + [Parameter()] + [AllowEmptyCollection()] + [Object[]]$Content, + + [Parameter()] + [int]$Depth = 3 + ) + + # Write initial verbose message + Write-Verbose "[Export-ObjectCache] Starting export process for cache type: $CacheType" + + # Use the Enviroment Variables to set the Cache Directory Path + if ($ENV:AZDODSC_CACHE_DIRECTORY) + { + $CacheDirectoryPath = Join-Path -Path $ENV:AZDODSC_CACHE_DIRECTORY -ChildPath "Cache" + } + else + { + Throw "The environment variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the variable to the path of the cache directory." + } + + try + { + $cacheFilePath = Join-Path -Path $CacheDirectoryPath -ChildPath "$CacheType.clixml" + + # Create cache directory if it does not exist + if (-not (Test-Path -Path $CacheDirectoryPath)) + { + Write-Verbose "[Export-ObjectCache] Creating cache directory at path: $CacheDirectoryPath" + New-Item -Path $CacheDirectoryPath -ItemType Directory | Out-Null + } + + # Save content to cache file + Write-Verbose "[Export-ObjectCache] Saving content to cache file: $cacheFilePath" + $Content | Export-Clixml -Depth $Depth -LiteralPath $cacheFilePath + + # Confirm completion of export process + Write-Verbose "[Export-ObjectCache] Export process completed successfully for cache type: $CacheType" + + } + catch + { + throw "[Export-ObjectCache] Failed to create cache for Azure DevOps API: $_" + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Find-CacheItem.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Find-CacheItem.ps1 new file mode 100644 index 000000000..1b8fe169b --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Find-CacheItem.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS +Searches for a CacheItem in a given list of cache items based on a filter. + +.DESCRIPTION +The Find-CacheItem function searches for a CacheItem in a given list of cache items based on a filter. It returns the matching CacheItem. + +.PARAMETER CacheList +The list of cache items to search in. This parameter is mandatory and accepts pipeline input. + +.PARAMETER Filter +The filter to apply when searching for the CacheItem. This parameter is mandatory and accepts a script block. + +.OUTPUTS +System.Management.Automation.PSObject +The matching CacheItem. + +.EXAMPLE +$cacheItems = Get-CacheItems +$filteredCacheItem = $cacheItems | Find-CacheItem -Filter { $_.Name -eq 'MyCacheItem' } +$filteredCacheItem +# Returns the CacheItem with the name 'MyCacheItem' from the list of cache items. + +.NOTES +Author: Michael Zanatta +Date: 2025-01-06 +#> +Function Find-CacheItem +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline)] + [Alias('Cache')] + [Object[]]$CacheList, + + [Parameter(Mandatory = $true)] + [ScriptBlock]$Filter + ) + + # Logging + Write-Verbose "[Find-CacheItem] Searching for the CacheItem with filter '$Filter'." + + # Get the CacheItem + $cacheItem = $null + $cacheItem = $CacheList | Where-Object -FilterScript $Filter + + # + # Return the CacheItem + return $cacheItem +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheItem.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheItem.ps1 new file mode 100644 index 000000000..ca059cffa --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheItem.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS +Get a cache item from the cache. + +.DESCRIPTION +Get a cache item from the cache. + +.PARAMETER Key +The key of the cache item to get. + +.EXAMPLE +Get-CacheItem -Key 'MyKey' + +.NOTES +This function is private and should not be used directly. +#> +function Get-CacheItem +{ + [CmdletBinding()] + [OutputType([CacheItem])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Key, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string] + $Type, + + [Parameter()] + [scriptblock] + $Filter + ) + + try + { + [System.Collections.Generic.List[CacheItem]]$cache = Get-CacheObject -CacheType $Type + $cacheItem = $cache.Where({$_.Key -eq $Key}) + } + catch + { + $cacheItem = $null + Write-Verbose $_ + } + + if ($null -eq $cacheItem) + { + return $null + } + + if ($Filter -ne $null) + { + $cacheItem = $cacheItem | Where-Object $Filter + } + + return $cacheItem.Value + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheObject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheObject.ps1 new file mode 100644 index 000000000..f6c32a2d8 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheObject.ps1 @@ -0,0 +1,83 @@ +<# +.SYNOPSIS +Retrieves a cache object of a specified type. + +.DESCRIPTION +The Get-CacheObject function is used to retrieve a cache object of a specified type. It first checks if the cache object is available in memory, and if not, it attempts to import it. The function supports different cache types such as Project, Team, Group, and SecurityDescriptor. + +.PARAMETER CacheType +Specifies the type of cache object to retrieve. Valid values are 'Project', 'Team', 'Group', and 'SecurityDescriptor'. + +.PARAMETER CacheRootPath +Specifies the root path of the cache. By default, it uses the path of the current script. + +.EXAMPLE +Get-CacheObject -CacheType Project +Retrieves the cache object of type 'Project'. + +.EXAMPLE + +Retrieves the cache object of type 'Team' from the specified root path. + +.INPUTS +None. + +.OUTPUTS +The cache object of the specified type. + +.NOTES +This function is part of the AzureDevOpsDsc module. + +.LINK +https://github.com/dsccommunity/AzureDevOpsDsc + +#> +function Get-CacheObject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string]$CacheType + ) + + # Write initial verbose message + Write-Verbose "[Get-ObjectCache] Attempting to retrieve cache object for type: $CacheType" + + # Use the Enviroment Variables to set the Cache Directory Path + if ($ENV:AZDODSC_CACHE_DIRECTORY) + { + $CacheDirectoryPath = Join-Path -Path $ENV:AZDODSC_CACHE_DIRECTORY -ChildPath "Cache" + } + else + { + Throw "The environment variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the variable to the path of the cache directory." + } + + try + { + # Attempt to get the variable from the global scope + $var = Get-Variable -Name "AzDo$CacheType" -Scope Global -ErrorAction SilentlyContinue + + if ($var) + { + Write-Verbose "[Get-ObjectCache] Cache object found in memory for type: $CacheType" + # If the variable is found, return the content of the cache. Dont use $var here, since it will a different object type. + $var = Get-Variable -Name "AzDo$CacheType" -ValueOnly -Scope Global + } + else + { + Write-Verbose "[Get-ObjectCache] Cache object not found in memory, attempting to import for type: $CacheType" + $var = Import-CacheObject -CacheType $CacheType + } + + # Return the content of the cache after importing it + Write-Verbose "[Get-ObjectCache] Returning imported cache object for type: $CacheDirectoryPath" + return $var + + } + catch + { + throw "[Get-ObjectCache] Failed to get cache for Azure DevOps API: $_" + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Import-CacheObject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Import-CacheObject.ps1 new file mode 100644 index 000000000..e45e146e4 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Import-CacheObject.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS +Imports a cache object for Azure DevOps API. + +.DESCRIPTION +The Import-CacheObject function is used to import a cache object for Azure DevOps API. It checks if the cache file exists and imports its content if found. The cache object is then stored in a global variable. + +.PARAMETER CacheType +Specifies the type of cache object to import. Valid values are 'Project', 'Team', 'Group', and 'SecurityDescriptor'. + +.PARAMETER CacheRootPath +Specifies the root path where the cache directory is located. By default, it uses the current script's root path. + +.EXAMPLE + + +This example imports the cache object for the 'Project' type from the cache directory located at "C:\Cache". + +.INPUTS +None. + +.OUTPUTS +None. + +.NOTES +#> + +function Import-CacheObject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string]$CacheType + ) + + # Write initial verbose message + Write-Verbose "[Import-CacheObject] Starting to import cache object for type: $CacheType" + + # Use the Enviroment Variables to set the Cache Directory Path + if ($ENV:AZDODSC_CACHE_DIRECTORY) + { + $CacheDirectoryPath = Join-Path -Path $ENV:AZDODSC_CACHE_DIRECTORY -ChildPath "Cache" + } + else + { + Throw "The environment variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the variable to the path of the cache directory." + } + + Write-Verbose "[Import-CacheObject] Cache root path: $CacheDirectoryPath" + + try + { + # Determine cache file path + $cacheFile = Join-Path -Path $CacheDirectoryPath -ChildPath "$CacheType.clixml" + + # Check if cache file exists + if (-not (Test-Path -Path $cacheFile)) + { + Write-Warning "[Import-CacheObject] Cache file not found at path: $cacheFile" + } + + Write-Verbose "[Import-CacheObject] Importing content from cache file at path: $cacheFile" + + $Content = Import-Clixml -Path $cacheFile + + #Set-Variable -Name "AzDo$CacheType" -Value $Content -Scope Global -Force + Write-Verbose "[Import-CacheObject] Successfully imported cache object for type: $cacheFile" + + # Convert the imported cache object to a list of CacheItem objects + $newCache = [System.Collections.Generic.List[CacheItem]]::New() + + # If the content is null, skip! + if ($null -ne $Content) + { + $Content | ForEach-Object { + # If the key is empty, skip the item + if ([string]::IsNullOrEmpty($_.Key)) + { + return + } + + # Create a new CacheItem object and add it to the list + $newCache.Add([CacheItem]::New($_.Key, $_.Value)) + } + } + + # Update the new cache object + Set-Variable -Name "AzDo$CacheType" -Value $newCache -Scope Global -Force + Write-Verbose "[Import-CacheObject] Cache object imported successfully for '$CacheType'." + + } + catch + { + throw "[Import-CacheObject] Failed to import cache for Azure DevOps API: $_" + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Initialize-CacheObject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Initialize-CacheObject.ps1 new file mode 100644 index 000000000..42a1a2071 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Initialize-CacheObject.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS + Initializes the cache object for Azure DevOps API. + +.DESCRIPTION + This function is used to initialize the cache object for Azure DevOps API. It checks if the cache file exists and imports the cache object if it does. If the cache file does not exist, it creates a new cache object. + +.PARAMETER CacheType + Specifies the type of cache to initialize. Valid values are 'Project', 'Team', 'Group', and 'SecurityDescriptor'. + +.EXAMPLE + Initialize-CacheObject -CacheType Project + Initializes the cache object for the 'Project' cache type. + +.NOTES +#> +Function Initialize-CacheObject +{ + [CmdletBinding()] + param( + # Specifies the type of cache to initialize. Valid values are 'Project', 'Team', 'Group', and 'SecurityDescriptor'. + [Parameter(Mandatory = $true,ValueFromPipeline)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string]$CacheType, + # Used to bypass the file deletion check for live caches. Needed for DSC Resources to import the cache. + [Parameter()] + [Switch]$BypassFileCheck + ) + + try + { + + # Use the Enviroment Variables to set the Cache Directory Path + if ($ENV:AZDODSC_CACHE_DIRECTORY) + { + $CacheDirectoryPath = Join-Path -Path $ENV:AZDODSC_CACHE_DIRECTORY -ChildPath "Cache" + } + else + { + Throw "The environment variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the variable to the path of the cache directory." + } + + $cacheFilePath = Join-Path -Path $CacheDirectoryPath -ChildPath "$CacheType.clixml" + Write-Verbose "[Initialize-CacheObject] Cache file path: $cacheFilePath" + + # If the cache group is LiveGroups or LiveProjects, set the cache file path to the temporary directory + if (-not($BypassFileCheck.IsPresent) -and ($CacheType -match '^Live')) + { + # Flush the cache if it is a live cache + if (Test-Path -LiteralPath $cacheFilePath -ErrorAction SilentlyContinue) + { + Write-Verbose "[Initialize-CacheObject] Cache file found. Removing cache file for '$CacheType'." + Remove-Item -LiteralPath $cacheFilePath -Force + } + } + else + { + # Test if the Cache File exists. If it exists, import the cache object + Write-Verbose "[Initialize-CacheObject] Cache file path: $cacheFilePath" + } + + # Test if the Cache File exists. If it exists, import the cache object + if (Test-Path -Path $cacheFilePath) + { + # If the cache file exists, import the cache object + Write-Verbose "[Initialize-CacheObject] Cache file found. Importing cache object for '$CacheType'." + Import-CacheObject -CacheType $CacheType + } + else + { + # If the cache file does not exist, create a new cache object + Write-Verbose "[Initialize-CacheObject] Cache file not found. Creating new cache object for '$CacheType'." + + # Create the cache directory if it does not exist + if (-not (Test-Path -Path $CacheDirectoryPath)) + { + Write-Verbose "[Initialize-CacheObject] Cache directory not found. Creating cache directory." + New-Item -Path $CacheDirectoryPath -ItemType Directory | Out-Null + } + + # Create the content + $content = [System.Collections.Generic.List[CacheItem]]::New() + + # Create a new cache object + Set-CacheObject -CacheType $CacheType -Content $content + + } + + } + catch + { + throw "[Initialize-CacheObject] Failed to import cache for Azure DevOps API: $_" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-AzDoCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-AzDoCache.ps1 new file mode 100644 index 000000000..a3a6b57e0 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-AzDoCache.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS +Refreshes the Azure DevOps cache by clearing existing cache variables and reinitializing them. + +.DESCRIPTION +The Refresh-AzDoCache function clears the current Azure DevOps cache variables and reinitializes them by invoking all caching commands from the AzureDevOpsDsc.Common module. It then reimports the cache objects to ensure the cache is up-to-date. + +.PARAMETER OrganizationName +Specifies the name of the Azure DevOps organization for which the cache should be refreshed. This parameter is mandatory. + +.EXAMPLE +Refresh-AzDoCache -OrganizationName "MyOrganization" +This example refreshes the Azure DevOps cache for the organization named "MyOrganization". + +.NOTES +This function is intended for internal use within the AzureDevOpsDsc.Common module to maintain the integrity of the cache. + +#> +Function Refresh-AzDoCache +{ + param( + [Parameter(Mandatory = $true)] + [string]$OrganizationName + ) + + # Clear the live cache + Get-Variable Azdo* -Scope Global | Remove-Variable -Scope Global + + # Iterate through Each of the Caching Commands and initalize the Cache. + Get-Command "AzDoAPI_*" | Where-Object Source -eq 'AzureDevOpsDsc.Common' | ForEach-Object { + . $_.Name -OrganizationName $OrganizationName + } + + # ReImport the Cache + Get-AzDoCacheObjects | ForEach-Object { + Import-CacheObject -CacheType $_ + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheIdentity.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheIdentity.ps1 new file mode 100644 index 000000000..c0d1a04d1 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheIdentity.ps1 @@ -0,0 +1,78 @@ +<# +.SYNOPSIS +Refreshes the cache identity for a given object. + +.DESCRIPTION +The Refresh-CacheIdentity function updates the cache identity for a specified object. It performs a lookup to get the ACL descriptor and adds the ACL identity to the object. The updated object is then added to the cache. + +.PARAMETER Identity +The object whose cache identity needs to be refreshed. This parameter is mandatory. + +.PARAMETER Key +The key associated with the cache item. This parameter is mandatory. + +.PARAMETER CacheType +The type of cache to update. This parameter is mandatory and must be one of the valid cache types returned by Get-AzDoCacheObjects. + +.EXAMPLE +$identity = Get-IdentityObject +$key = "someKey" +$cacheType = "someCacheType" +Refresh-CacheIdentity -Identity $identity -Key $key -CacheType $cacheType + +.NOTES +This function relies on the global variable $Global:DSCAZDO_OrganizationName and the functions Get-DevOpsDescriptorIdentity, Add-CacheItem, Get-CacheObject, and Set-CacheObject. + +#> +Function Refresh-CacheIdentity +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Object]$Identity, + [Parameter(Mandatory = $true)] + [String]$Key, + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [String]$CacheType + ) + + # + # Perform a lookup to get the ACL Descriptor + + $params = @{ + OrganizationName = $Global:DSCAZDO_OrganizationName + SubjectDescriptor = $Identity.descriptor + } + + $descriptorIdentity = Get-DevOpsDescriptorIdentity @params + + # Add the ACLIdentity to the object + $ACLIdentity = [PSCustomObject]@{ + id = $descriptorIdentity.id + descriptor = $descriptorIdentity.descriptor + subjectDescriptor = $descriptorIdentity.subjectDescriptor + providerDisplayName = $descriptorIdentity.providerDisplayName + isActive = $descriptorIdentity.isActive + isContainer = $descriptorIdentity.isContainer + } + + $Identity | Add-Member -MemberType NoteProperty -Name 'ACLIdentity' -Value $ACLIdentity -Force + + # + # Add the object to the cache + $cacheParams = @{ + Key = $Key + Value = $Identity + Type = $CacheType + SuppressWarning = $true + } + + # Add to the cache + Add-CacheItem @cacheParams + + # Update the cache object + $currentCache = Get-CacheObject -CacheType $CacheType + Set-CacheObject -Content $currentCache -CacheType $CacheType + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheObject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheObject.ps1 new file mode 100644 index 000000000..f3f8fbe0b --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheObject.ps1 @@ -0,0 +1,42 @@ +<# +.SYNOPSIS + Unloads and reloads the cache object of the specified type. + +.DESCRIPTION + The Refresh-CacheObject function is used to unload and then reload a cache object of a specified type. + This can be useful when you need to refresh the state of a cache object to ensure it is up-to-date. + +.PARAMETER CacheType + The type of the cache object to be refreshed. This parameter is mandatory and must be one of the valid cache object types returned by the Get-AzDoCacheObjects function. + +.EXAMPLE + Refresh-CacheObject -CacheType 'Project' + This example unloads and reloads the cache object of type 'Project'. + +.NOTES + The function uses the Remove-Variable cmdlet to unload the cache object and the Import-CacheObject function to reload it. + Verbose messages are written to provide feedback on the unloading and reloading process. + +#> +# Unloads and reloads the cache object of the specified type. +Function Refresh-CacheObject +{ + param ( + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string] + $CacheType + ) + + Write-Verbose "[Refresh-CacheObject] Unloading the cache object of type '$CacheType'." + + # Unload the current cache object + Remove-Variable -Name "AzDo$CacheType" -Scope Global -ErrorAction SilentlyContinue + + Write-Verbose "[Refresh-CacheObject] Reloading the cache object of type '$CacheType'." + + # Reload the cache object + Import-CacheObject -CacheType $CacheType + +} + diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Remove-CacheItem.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Remove-CacheItem.ps1 new file mode 100644 index 000000000..f53b4ef82 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Remove-CacheItem.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS +Removes an item from the Azure DevOps cache. + +.DESCRIPTION +The Remove-CacheItem function is used to remove an item from the Azure DevOps cache. It takes a key and a type as parameters. The key is the identifier of the item to be removed, and the type specifies the type of cache (Project, Team, Group, or SecurityDescriptor) from which the item should be removed. + +.PARAMETER Key +The key of the item to be removed from the cache. + +.PARAMETER Type +The type of cache from which the item should be removed. Valid values are 'Project', 'Team', 'Group', and 'SecurityDescriptor'. + +.EXAMPLE +Remove-CacheItem -Key "myKey" -Type "Project" +Removes the item with the key "myKey" from the Project cache. + +.EXAMPLE +Remove-CacheItem -Key "anotherKey" -Type "Group" +Removes the item with the key "anotherKey" from the Group cache. +#> +Function Remove-CacheItem +{ + param ( + [Parameter(Mandatory = $true)] + [string] + $Key, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string] + $Type + ) + + Write-Verbose "[Remove-CacheItem] Retrieving the current cache." + #$cache = Get-AzDevOpsCache -CacheType $Type + [System.Collections.Generic.List[CacheItem]]$cache = Get-CacheObject -CacheType $Type + + Write-Verbose "[Remove-CacheItem] Removing the cache item with the key: '$Key'." + + # If the cache has a length of 1, and the key matches, remove the cache + if ($cache.Count -eq 1 -and $cache[0].Key -eq $Key) + { + Write-Verbose "[Remove-CacheItem] Cache has a length of 1 and the key matches. Removing the cache." + Set-Variable -Name "AzDo$Type" -Value ([System.Collections.Generic.List[CacheItem]]::New()) -Scope Global + return + } + + # Remove the item from the cache + 0 .. $cache.Count | Where-Object { $cache[$_].Key -eq $Key } | ForEach-Object { $cache.RemoveAt($_) } + + # Update the memory cache + Set-Variable -Name "AzDo$Type" -Value $cache -Scope Global + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Set-CacheObject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Set-CacheObject.ps1 new file mode 100644 index 000000000..9f455c199 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Set-CacheObject.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS +Sets a cache object for Azure DevOps API. + +.DESCRIPTION +The Set-CacheObject function is used to set a cache object for Azure DevOps API. It creates a cache directory if it does not exist, saves the content to a cache file, and sets the content to a global variable. + +.PARAMETER CacheType +Specifies the type of cache object. Valid values are 'Project', 'Team', 'Group', and 'SecurityDescriptor'. + +.PARAMETER Content +Specifies the content to be cached. This should be an array of objects. + +.PARAMETER Depth +Specifies the depth of the object to be serialized. Default value is 3. + +.PARAMETER CacheRootPath +Specifies the root path for the cache directory. Default value is the script root path. + +.EXAMPLE +Set-CacheObject -CacheType 'Project' -Content $projectData -Depth 2 + +This example sets a cache object for the 'Project' type with the provided project data, using a serialization depth of 2. + +.INPUTS +None. + +.OUTPUTS +None. + +.NOTES +Author: Michael Zanatta +Date: 2025-01-06 +#> + +function Set-CacheObject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AzDoCacheObjects)})] + [string]$CacheType, + + [Parameter()] + [AllowEmptyCollection()] + [Object[]]$Content, + + [Parameter()] + [int]$Depth = 3 + ) + + # Write initial verbose message + Write-Verbose "[Set-ObjectCache] Starting to set cache object for type: $CacheType" + + try + { + # Save content to cache file + Write-Verbose "[Set-ObjectCache] Exporting content to cache file for type: $CacheType" + Export-CacheObject -CacheType $CacheType -Content $Content -Depth $Depth + + # Save content to global variable + Write-Verbose "[Set-ObjectCache] Setting global variable AzDo$CacheType with the provided content" + Set-Variable -Name "AzDo$CacheType" -Value $Content -Scope Global -Force + + Write-Verbose "[Set-ObjectCache] Successfully set cache object for type: $CacheType" + + } + catch + { + throw "[Set-ObjectCache] Failed to create cache for Azure DevOps API: $_" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResource.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResource.ps1 deleted file mode 100644 index 95f93cde1..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResource.ps1 +++ /dev/null @@ -1,107 +0,0 @@ -<# - .SYNOPSIS - Returns an array of resources returned from the Azure DevOps API. The type of resource - returned is generic to make this function reusable across all resources from the API. - - The resource type requested from the API is determined by the 'ResourceName' parameter. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER ApiVersion - The version of the Azure DevOps API to use in the call/execution to/against the API. - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ResourceName - The name of the resource being obtained from the Azure DevOps API (e.g. 'Project' or 'Operation') - - .PARAMETER ResourceId - The 'id' of the resource type being obtained. For example, if the 'ResourceName' parameter value - was 'Project', the 'ResourceId' value would be assumed to be the 'id' of a 'Project'. - - .EXAMPLE - Get-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' - - Returns all 'Project' resources from the Azure DevOps API related to the Organization/ApiUri - value provided. - - .EXAMPLE - Get-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' -ResourceId 'YourProjectId' - - Returns the 'Project' resource from the Azure DevOps API related to the Organization/ApiUri - value provided (where the 'id' of the 'Project' is equal to 'YourProjectId'). -#> -function Get-AzDevOpsApiResource -{ - [CmdletBinding()] - [OutputType([System.Management.Automation.PSObject[]])] - param - ( - [Parameter(Mandatory=$true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter()] - [ValidateScript( { Test-AzDevOpsApiVersion -ApiVersion $_ -IsValid })] - [System.String] - $ApiVersion = $(Get-AzDevOpsApiVersion -Default), - - [Parameter(Mandatory=$true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory=$true)] - [ValidateScript({ Test-AzDevOpsApiResourceName -ResourceName $_ -IsValid })] - [System.String] - $ResourceName, - - [Parameter()] - [ValidateScript({ Test-AzDevOpsApiResourceId -ResourceId $_ -IsValid })] - [System.String] - $ResourceId - ) - - - # Prepare 'Get-AzDevOpsApiResourceUri' method parameters - $apiResourceUriParameters = @{ - ApiUri = $ApiUri - ApiVersion = $ApiVersion - ResourceName = $ResourceName - } - - if (![System.String]::IsNullOrWhiteSpace($ResourceId)) - { - $apiResourceUriParameters.ResourceId = $ResourceId - } - - - # Prepare 'Invoke-AzDevOpsApiRestMethod' method parameters - $invokeRestMethodParameters = @{ - Uri = $(Get-AzDevOpsApiResourceUri @apiResourceUriParameters) - Method = 'Get' - Headers = $(Get-AzDevOpsApiHttpRequestHeader -Pat $Pat) - } - - - [System.Management.Automation.PSObject]$apiResources = Invoke-AzDevOpsApiRestMethod @invokeRestMethodParameters - - - # If not a single, resource request, set from the resource(s) in the 'value' property within the response - if ($null -ne $apiResources.value) - { - [System.Management.Automation.PSObject[]]$apiResources = $apiResources.value - } - - - return $apiResources -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACEList.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACEList.ps1 new file mode 100644 index 000000000..d83f2d523 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACEList.ps1 @@ -0,0 +1,102 @@ +<# +.SYNOPSIS +Converts permissions to an Access Control Entry (ACE) token. + +.DESCRIPTION +The ConvertTo-ACEList function converts permissions to an Access Control Entry (ACE) token. It takes the security namespace, identity, an array of permissions objects, and the organization name as mandatory parameters. It constructs the ACE token for each permission and adds it to the list of ACEs. + +.PARAMETER SecurityNamespace +The security namespace as a string. This parameter is mandatory. + +.PARAMETER Identity +The identity associated with the ACE. This parameter is mandatory. + +.PARAMETER Permissions +An array of permissions objects. This parameter is mandatory. + +.PARAMETER OrganizationName +The organization name as a string. This parameter is mandatory. + +.EXAMPLE +ConvertTo-ACEList -SecurityNamespace "Namespace" -Identity "User1" -Permissions @("Read", "Write") -OrganizationName "MyOrg" + +This example converts the permissions "Read" and "Write" for the identity "User1" in the specified security namespace and organization name to an ACE token. + +.NOTES +Author: Michael Zanatta +Date: 2025-01-06 +#> + +Function ConvertTo-ACEList +{ + [CmdletBinding()] + param ( + # Mandatory parameter: the security namespace as a string. + [Parameter(Mandatory = $true)] + [string]$SecurityNamespace, + + # Mandatory parameter: an array of permissions objects. + [Parameter()] + [Object[]]$Permissions = @(), + + # Mandatory parameter: the organization name as a string. + [Parameter(Mandatory = $true)] + [string]$OrganizationName + ) + + # Log the start of the function. + Write-Verbose "[ConvertTo-ACEList] Started." + Write-Verbose "[ConvertTo-ACEList] Security Namespace: $SecurityNamespace" + Write-Verbose "[ConvertTo-ACEList] Organization Name: $OrganizationName" + Write-Verbose "[ConvertTo-ACEList] Permissions: $($Permissions | ConvertTo-Json)" + + # Initialize an empty list to hold the ACEs (Access Control Entries). + $ACEs = [System.Collections.Generic.List[HashTable]]::new() + + # Iterate through each of the permissions and construct the ACE token. + ForEach ($Permission in $Permissions) + { + + Write-Verbose "[ConvertTo-ACEList] Constructing ACE for $($Permission.Identity)." + + # Define the parameters for the Find-Identity function. + $identityParams = @{ + # The name of the identity to search for. Remove any square brackets (e.g., [TEAM FOUNDATION]\Project Collection Administrators). + Name = $Permission.Identity.Replace('[', '').Replace(']', '') + OrganizationName = $OrganizationName + SearchType = 'principalName' + } + + # Define the parameters for the ACE Token. + $aceTokenParams = @{ + SecurityNamespace = $SecurityNamespace + ACEPermissions = $Permission.Permission + } + + # Convert the Permission to an ACE. + $ht = @{ + Identity = Find-Identity @identityParams + Permissions = ConvertTo-ACETokenList @aceTokenParams + } + + # If the Identity is not found, log a warning. + if (-not $ht.Identity) + { + Write-Warning "[ConvertTo-ACEList] Identity $($Permission.Identity) was not found. This will not be added to the ACEs list." + continue + } + # If the Permissions are not found, log a warning. + if (-not $ht.Permissions) + { + Write-Warning "[ConvertTo-ACEList] Permissions for $($Permission.Identity) were not found. This will not be added to the ACEs list." + continue + } + + # Add the constructed ACE to the ACEs list. + $ACEs.Add($ht) + + } + + return $ACEs + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACETokenList.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACETokenList.ps1 new file mode 100644 index 000000000..5a9d899eb --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACETokenList.ps1 @@ -0,0 +1,70 @@ + + +Function ConvertTo-ACETokenList +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$SecurityNamespace, + + [Parameter(Mandatory = $true)] + [Object[]]$ACEPermissions + ) + + Write-Verbose "[ConvertTo-ACETokenList] Initializing the ACL Token." + $hashTableArray = [System.Collections.Generic.List[HashTable]]::new() + + Write-Verbose "[ConvertTo-ACETokenList] Performing a Lookup for the Security Descriptor." + Write-Verbose "[ConvertTo-ACETokenList] Security Namespace: $SecurityNamespace" + + $SecurityDescriptor = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces' + + # Check if the Security Descriptor was found + if (-not $SecurityDescriptor) + { + Write-Error "Security Descriptor not found for namespace: $SecurityNamespace" + return + } + + # Iterate through each of the ACEs and construct the ACE Object + Write-Verbose "[ConvertTo-ACETokenList] Iterating through each of the ACE Permissions." + + ForEach ($ACEPermission in $ACEPermissions) + { + # Check to see if there are any permissions that are not found in the Security Descriptor + $missingPermissions = $ACEPermission.Keys | Where-Object { + ($_ -notin $SecurityDescriptor.actions.displayName) -and + ($_ -notin $SecurityDescriptor.actions.name) + } | ForEach-Object { + Write-Verbose "[ConvertTo-ACETokenList] Permission '$_' not found in the Security Descriptor for namespace: $SecurityNamespace" + } + + # Filter the Allow and Deny permissions + Write-Verbose "[ConvertTo-ACETokenList] ACEPermission: $($ACEPermission | ConvertTo-Json)" + Write-Verbose "[ConvertTo-ACETokenList] Filtering Allow and Deny permissions." + + $AllowPermissions = $ACEPermission.Keys | Where-Object { $ACEPermission."$_" -eq 'Allow' } + $DenyPermissions = $ACEPermission.Keys | Where-Object { $ACEPermission."$_" -eq 'Deny' } + + Write-Verbose "[ConvertTo-ACETokenList] Iterating through the Allow and Deny Permissions and computing actions." + $AllowBits = $SecurityDescriptor.actions | Where-Object { ($_.displayName -in $AllowPermissions) -or ($_.name -in $AllowPermissions) } + $DenyBits = $SecurityDescriptor.actions | Where-Object { ($_.displayName -in $DenyPermissions) -or ($_.name -in $DenyPermissions) } + + # Compute the bitwise OR for the permissions + $hashTable = @{ + DescriptorType = $SecurityNamespace + Allow = $AllowBits + Deny = $DenyBits + } + + Write-Verbose "[ConvertTo-ACETokenList] Adding computed hash table to the array" + Write-Verbose "[ConvertTo-ACETokenList] Hash Table: $($hashTable | ConvertTo-Json)" + $hashTableArray.Add($hashTable) + } + + Write-Verbose "[ConvertTo-ACETokenList] Completed processing ACE Permissions" + + # Return the hashtable array + return $hashTableArray + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACL.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACL.ps1 new file mode 100644 index 000000000..d57b20d83 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACL.ps1 @@ -0,0 +1,113 @@ +<# +.SYNOPSIS +Converts an array of hash tables containing permissions into a list of Access Control Lists (ACLs). + +.DESCRIPTION +The ConvertTo-ACL function takes an array of hash tables containing permissions and converts them into a list of Access Control Lists (ACLs). Each permission in the array must contain 'Identity' and 'Permissions' keys. The function creates an ACL token and ACE (Access Control Entry) for each permission, and then adds them to the ACL list. The ACL list is returned as the output of the function. + +.PARAMETER Permissions +Mandatory parameter. An array of hash tables containing permissions. Each permission must contain 'Identity' and 'Permissions' keys. + +.PARAMETER SecurityNamespace +Mandatory parameter. The security namespace as a string. + +.PARAMETER isInherited +Mandatory parameter. Boolean indicating if the ACL is inherited. + +.PARAMETER OrganizationName +Mandatory parameter. The organization name as a string. + +.EXAMPLE +$permissions = @( + @{ + Identity = 'User1' + Permissions = 'Read' + }, + @{ + Identity = 'User2' + Permissions = 'Read', 'Write' + } +) + +ConvertTo-ACL -Permissions $permissions -SecurityNamespace 'Namespace1' -isInherited $true -OrganizationName 'Org1' + +This example converts an array of permissions into ACLs for a specific security namespace and organization. + +.OUTPUTS +System.Collections.Generic.List[HashTable] +A list of Access Control Lists (ACLs) created from the provided permissions. + +.NOTES +Author: Michael Zanatta +Date: 2025-01-06 +#> +Function ConvertTo-ACL +{ + [CmdletBinding()] + param ( + # Mandatory parameter: an array of hash tables containing permissions. + [Parameter()] + [HashTable[]]$Permissions=@(), + + # Mandatory parameter: the security namespace as a string. + [Parameter(Mandatory = $true)] + [string]$SecurityNamespace, + + # Mandatory parameter: boolean indicating if the ACL is inherited. + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + # Mandatory parameter: the organization name as a string. + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + # Mandatory parameter: the token name as a string. + [Parameter(Mandatory = $true)] + [string]$TokenName + ) + + # Verbose output indicating the start of the function. + Write-Verbose "[ConvertTo-ACL] Started." + + # Create a hash table for ACL token parameters. + $ACLTokenParams = @{ + SecurityNamespace = $SecurityNamespace + TokenName = $TokenName + } + + Write-Verbose "[ConvertTo-ACL] ACL Token Params: $($ACLTokenParams | Out-String)" + + # Create a hash table for ACE parameters. + $ACEParams = @{ + SecurityNamespace = $SecurityNamespace + Permissions = $Permissions + OrganizationName = $OrganizationName + } + + Write-Verbose "[ConvertTo-ACL] ACE Params: $($ACEParams | Out-String)" + + # Convert the Permission to an ACL Token and create the ACL hash table. + $ACL = @{ + token = New-ACLToken @ACLTokenParams + aces = ConvertTo-ACEList @ACEParams + inherited = $isInherited + } + + # If the ACEs are empty, write a warning and return. + if ($ACL.aces.Count -eq 0) + { + Write-Warning "[ConvertTo-ACL] No ACEs were created. Returning." + return $ACL + } + + # Group the ACEs by the identity removing any duplicates. + $ACL.aces = Group-ACEs -ACEs $ACL.aces + + Write-Verbose "[ConvertTo-ACL] Created ACL: $($ACL | Out-String)" + + # Verbose output indicating the completion of the function. + Write-Verbose "[ConvertTo-ACL] Completed. Returning ACLs." + + # Return the list of ACLs. + return $ACL +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACLHashtable.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACLHashtable.ps1 new file mode 100644 index 000000000..a9c7ed078 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACLHashtable.ps1 @@ -0,0 +1,169 @@ +<# +.SYNOPSIS +Serializes the ACL list based on the provided reference ACLs, descriptor ACL list, and descriptor match token. + +.DESCRIPTION +The ConvertTo-ACLHashtable function captures the ACLs that are relevant to the Git Repository by matching the descriptor match token. If the descriptor ACL list is empty, it falls back to using the reference ACLs. It then constructs an ACL object for each ACL in the ACL list, including properties like inheritPermissions, token, and acesDictionary. The acesDictionary contains ACE objects with properties like allow, deny, and descriptor. Finally, it returns the constructed ACLs hashtable. + +.PARAMETER ReferenceACLs +The reference ACLs to be used as a fallback if the descriptor ACL list is empty. + +.PARAMETER DescriptorACLList +The descriptor ACL list containing the ACLs relevant to the Git Repository. + +.PARAMETER DescriptorMatchToken +The descriptor match token used to filter the ACLs relevant to the Git Repository. + +.EXAMPLE +$referenceACLs = @( + [PSCustomObject]@{ + token = "token1" + inheritPermissions = $true + aces = @( + [PSCustomObject]@{ + permissions = @{ + allow = @{ + bit = 1 + } + deny = @{ + bit = 0 + } + } + Identity = @{ + value = @{ + ACLIdentity = @{ + descriptor = "descriptor1" + } + } + } + } + ) + } +) + +$descriptorACLList = @( + [PSCustomObject]@{ + token = "token2" + inheritPermissions = $false + aces = @( + [PSCustomObject]@{ + permissions = @{ + allow = @{ + bit = 0 + } + deny = @{ + bit = 1 + } + } + Identity = @{ + value = @{ + ACLIdentity = @{ + descriptor = "descriptor2" + } + } + } + } + ) + } +) + +$descriptorMatchToken = "token2" + +ConvertTo-ACLHashtable -ReferenceACLs $referenceACLs -DescriptorACLList $descriptorACLList -DescriptorMatchToken $descriptorMatchToken + +.OUTPUTS +System.Collections.Hashtable +The constructed ACLs hashtable containing the serialized ACLs. +#> + +Function ConvertTo-ACLHashtable +{ + param( + [Parameter()] + [Object[]] + $ReferenceACLs, + + [Parameter(Mandatory = $true)] + [Object[]] + $DescriptorACLList, + + [Parameter(Mandatory = $true)] + [String] + $DescriptorMatchToken + ) + + # Convert the ReferenceACLs to an array if it is not already an array + $ReferenceACLs = [Array]$ReferenceACLs + + Write-Verbose "[ConvertTo-ACLHashtable] Started." + Write-Verbose "[ConvertTo-ACLHashtable] Reference ACLs: $($ReferenceACLs | ConvertTo-Json -Depth 3)" + Write-Verbose "[ConvertTo-ACLHashtable] Descriptor ACL List: $($DescriptorACLList | ConvertTo-Json -Depth 3)" + Write-Verbose "[ConvertTo-ACLHashtable] Descriptor Match Token: $DescriptorMatchToken" + + # Initialize the ACLs hashtable with a count and a list to hold ACL objects + Write-Verbose "[ConvertTo-ACLHashtable] Initializing the ACLs hashtable." + $ACLHashtable = @{ + Count = 0 + value = [System.Collections.Generic.List[Object]]::new() + } + + # Filter out all ACLs that don't match the descriptor match token. These are needed to construct the ACLs object + # Otherwise, the existing ACLs will be removed. + Write-Verbose "[ConvertTo-ACLHashtable] Filtering descriptor ACLs that do not match the descriptor match token." + $FilteredDescriptorACLs = $DescriptorACLList | Where-Object { $_.token -notmatch $DescriptorMatchToken } + + # Iterate through the filtered descriptor ACLs to construct the ACLs object + Write-Verbose "[ConvertTo-ACLHashtable] Iterating through the filtered descriptor ACLs to construct the ACLs object." + ForEach ($DescriptorACL in $FilteredDescriptorACLs) + { + Write-Verbose "Adding filtered ACL to the ACLs object." + $ACLHashtable.value.Add($DescriptorACL) + } + + # Construct the ACLs object from the reference ACLs + Write-Verbose "[ConvertTo-ACLHashtable] Constructing the ACLs object from the reference ACLs." + + # Iterate through the ACLs in the ReferenceACLs + ForEach ($ReferenceACL in $ReferenceACLs) + { + Write-Verbose "[ConvertTo-ACLHashtable] Processing reference ACL." + Write-Verbose "[ConvertTo-ACLHashtable] Reference ACL: $($ReferenceACL | ConvertTo-Json -Depth 3)" + + # Construct the ACL Object with properties inheritPermissions, token, and acesDictionary + $ACLObject = [PSCustomObject]@{ + inheritPermissions = $ReferenceACL.inherited + token = ConvertTo-FormattedToken -Token $ReferenceACL.token + acesDictionary = @{} + } + + # Iterate through the ACEs in the current ACL to construct the ACEs Dictionary + ForEach ($ACE in $ReferenceACL.aces) + { + Write-Verbose "[ConvertTo-ACLHashtable] Constructing ACE Object." + + # Construct the ACE Object with properties allow, deny, and descriptor + $ACEObject = @{ + allow = Get-BitwiseOrResult $ACE.permissions.allow.bit + deny = Get-BitwiseOrResult $ACE.permissions.deny.bit + descriptor = $ACE.Identity.value.ACLIdentity.descriptor + } + # Add the ACE to the ACEs Dictionary using the descriptor as the key + Write-Verbose "[ConvertTo-ACLHashtable] ACEObject $($ACEObject | ConvertTo-Json)." + Write-Verbose "[ConvertTo-ACLHashtable] Adding ACE to the ACEs Dictionary." + + $ACLObject.acesDictionary.Add($ACE.Identity.value.ACLIdentity.descriptor, $ACEObject) + } + + # Add the constructed ACL object (ACLObject) to the ACL List + Write-Verbose "[ConvertTo-ACLHashtable] Adding constructed ACL object to the ACL List." + $ACLHashtable.value.Add($ACLObject) + } + + # Update the ACL Count with the number of ACLs in the list + Write-Verbose "[ConvertTo-ACLHashtable] Updating the ACL Count." + $ACLHashtable.Count = $ACLHashtable.value.Count + + # Return the constructed ACLs hashtable + Write-Verbose "[ConvertTo-ACLHashtable] Completed." + Write-Output $ACLHashtable +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedACL.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedACL.ps1 new file mode 100644 index 000000000..48ca8293a --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedACL.ps1 @@ -0,0 +1,121 @@ +<# +.SYNOPSIS +Formats an Access Control List (ACL) object. + +.DESCRIPTION +The ConvertTo-FormattedACL function takes an ACL object and formats it into a structured format. It matches identities, formats permissions, and creates a formatted ACL object. + +.PARAMETER ACL +The ACL object from the pipeline. + +.PARAMETER SecurityNamespace +The security namespace as a string. + +.PARAMETER OrganizationName +The organization name as a string. + +.EXAMPLE +$myACL = Get-ACL -Path "C:\Temp" +$formattedACL = $myACL | ConvertTo-FormattedACL -SecurityNamespace "MyNamespace" -OrganizationName "MyOrganization" + +This example retrieves an ACL object for a specific path and formats it using the ConvertTo-FormattedACL function. + +.OUTPUTS +[System.Collections.Generic.List[HashTable]] +A list of formatted ACLs. + +.NOTES +Author: Michael Zanatta +Date: 2025-01-06 +#> + +Function ConvertTo-FormattedACL +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Object]$ACL, + + [Parameter(Mandatory = $true)] + [String]$SecurityNamespace, + + [Parameter(Mandatory = $true)] + [String]$OrganizationName + ) + + begin + { + Write-Verbose "[ConvertTo-FormattedACL] Started." + $ACLList = [System.Collections.Generic.List[HashTable]]::new() + } + + process + { + # Logging + Write-Verbose "[ConvertTo-FormattedACL] Processing ACL: $($ACL.token)" + Write-Verbose "[ConvertTo-FormattedACL] ACL: $($ACL | ConvertTo-Json)" + + # If the token is empty, skip it. + if (-not $ACL.token) + { + Write-Verbose "[ConvertTo-FormattedACL] Current token is empty. Skipping." + Write-Warning "[ConvertTo-FormattedACL] Current token is empty. Skipping. ACL: $($ACL | ConvertTo-Json)" + return + } + + $ACEs = [System.Collections.Generic.List[HashTable]]::new() + $ACEEntries = $ACL.acesDictionary.psObject.properties.name + Write-Verbose "[ConvertTo-FormattedACL] Found ACE entries: $($ACEEntries.Count)" + + # If the ACE entries are empty, skip it. + if ($ACEEntries.Count -eq 0) + { + Write-Verbose "[ConvertTo-FormattedACL] Current ACE entries are empty. Skipping." + Write-Warning "[ConvertTo-FormattedACL] Current ACE entries are empty. Skipping. ACL: $($ACL | ConvertTo-Json)" + return + } + + $ACEEntries | ForEach-Object { + Write-Verbose "[ConvertTo-FormattedACL] Processing ACE entry: $_" + $ACEs.Add([HashTable]@{ + Name = $_ + Value = $ACL.acesDictionary."$_" + }) + } + Write-Verbose "[ConvertTo-FormattedACL] Found ACEs: $($ACEs.Count)" + + # If the ACEs are empty, skip it. + if ($ACEs.Count -eq 0) + { + Write-Verbose "[ConvertTo-FormattedACL] Current ACEs are empty. Skipping." + Write-Warning "[ConvertTo-FormattedACL] Current ACEs are empty. Skipping. ACL: $($ACL | ConvertTo-Json)" + return + } + + # Create the Formatted ACL Object + foreach ($ACE in $ACEs) + { + Write-Verbose "[ConvertTo-FormattedACL] Matching identity for ACE: $($ACE.Name)" + $ACE."Identity" = Find-Identity -Name $ACE.Name -OrganizationName $OrganizationName + Write-Verbose "[ConvertTo-FormattedACL] Formatting ACE: $($ACE.Name) - Allow $($ACE.value.allow) - Deny $($ACE.value.deny)" + $ACE."Permissions" = Format-ACEs -Allow $ACE.value.allow -Deny $ACE.value.deny -SecurityNamespace $SecurityNamespace + } + + Write-Verbose "[ConvertTo-FormattedACL] Adding formatted ACL: $($ACL.token)" + + $formattedACL = [HashTable]@{ + token = Parse-ACLToken -Token $ACL.token -SecurityNamespace $SecurityNamespace + ACL = $ACL + inherited = $ACL.inheritPermissions + aces = $ACEs + } + + $ACLList.Add($formattedACL) + } + + end + { + Write-Verbose "[ConvertTo-FormattedACL] Completed." + return $ACLList + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedToken.ps1 new file mode 100644 index 000000000..9a097ac04 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedToken.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS +Formats the token based on its type. + +.DESCRIPTION +The ConvertTo-FormattedToken function is used to format a token based on its type. It takes a token as input and returns the formatted token string. + +.PARAMETER Token +The token to format. This parameter is mandatory and accepts an array of objects. + +.EXAMPLE +$token = @{ + type = 'GitProject' + projectId = 'myProject' + repositoryId = 'myRepo' +} +ConvertTo-FormattedToken -Token $token +# Output: "repoV2/myProject/myRepo" + +.NOTES +This function assumes that the token type is either 'GitOrganization', 'GitProject', or 'GitRepository'. If the token type is not one of these, the function will not format the token and will return an empty string. +#> + +Function ConvertTo-FormattedToken { + [CmdletBinding()] + param ( + # Define a mandatory parameter named 'Token' of type Object array + [Parameter(Mandatory = $true)] + [Object[]]$Token + ) + + # Output verbose message indicating the function has started + Write-Verbose "[ConvertTo-FormattedToken] Started." + + # Initialize variable to store formatted token string + $string = "" + + # Determine the type of the token and format accordingly + switch ($Token) + { + # If the token type is 'GitOrganization' + {$_.type -eq 'GitOrganization'} { + $string = 'repoV2' + break + } + # If the token type is 'GitProject' + {$_.type -eq 'GitProject'} { + $string = 'repoV2/{0}' -f $Token.projectId + break + } + # If the token type is 'GitRepository' + {$_.type -eq 'GitRepository'} { + $string = 'repoV2/{0}/{1}' -f $Token.projectId, $Token.RepoId + break + } + } + + # Output verbose message with the token value + Write-Verbose "[ConvertTo-FormattedToken] Token: $string" + + # Return the formatted token string + return $string +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Format-ACEs.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Format-ACEs.ps1 new file mode 100644 index 000000000..f47891793 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Format-ACEs.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS +Formats the Access Control Entries (ACEs) based on the specified parameters. + +.DESCRIPTION +The Format-ACEs function is used to format the Access Control Entries (ACEs) based on the specified parameters. It performs a lookup of the security namespace and creates a new ACE object with the allow and deny actions. + +.PARAMETER Allow +Specifies whether to include ACEs with the "Allow" permission. Default value is 0 (false). + +.PARAMETER Deny +Specifies whether to include ACEs with the "Deny" permission. Default value is 0 (false). + +.PARAMETER SecurityNamespace +Specifies the security namespace to perform the lookup. This parameter is mandatory. + +.EXAMPLE +Format-ACEs -Allow $true -Deny $false -SecurityNamespace "MySecurityNamespace" +Returns the ACE object with the "Allow" actions from the specified security namespace. + +.EXAMPLE +Format-ACEs -Allow $false -Deny $true -SecurityNamespace "MySecurityNamespace" +Returns the ACE object with the "Deny" actions from the specified security namespace. + +.NOTES +This function requires the Get-CacheItem cmdlet from the AzureDevOpsDsc.Common module to perform the security namespace lookup. +#> + +Function Format-ACEs +{ + [CmdletBinding()] + param + ( + [Parameter()] + [Int]$Allow=0, + [Parameter()] + [Int]$Deny=0, + [Parameter(Mandatory = $true)] + [string]$SecurityNamespace + ) + + # + # Logging + Write-Verbose "[Format-ACEs] Started." + + # + # Perform a Lookup of the Security Namespace + $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces' + + # Create a new ACE Object + $ACE = @{ + Allow = $namespace.actions | Where-Object { $_.bit -band $Allow } + Deny = $namespace.actions | Where-Object { $_.bit -band $Deny } + DescriptorType = $SecurityNamespace + } + + # + # Return the ACE + return $ACE +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Get-BitwiseOrResult.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Get-BitwiseOrResult.ps1 new file mode 100644 index 000000000..998433216 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Get-BitwiseOrResult.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Performs a bitwise OR operation on an array of integers. + +.DESCRIPTION + The Get-BitwiseOrResult function takes an array of integers as input and performs a bitwise OR operation on them. It returns the result of the operation. + +.PARAMETER integers + Specifies the array of integers on which the bitwise OR operation is performed. + + Required? true + Position? 1 + Default value + Accept pipeline input? false + Accept wildcard characters? false + +.EXAMPLE + $inputArray = 1, 2, 4, 8 + $result = Get-BitwiseOrResult -integers $inputArray + $result + # Output: 15 + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 +#> +Function Get-BitwiseOrResult +{ + [CmdletBinding()] + param ( + [int[]]$integers + ) + + Write-Verbose "[Get-BitwiseOrResult] Started." + Write-Verbose "[Get-BitwiseOrResult] Integers: $integers" + + $result = 0 + + if ($integers.Count -eq 0) + { + return 0 + } + + foreach ($integer in $integers) + { + if (-not [int]::TryParse($integer.ToString(), [ref]$null)) + { + Write-Error "Invalid integer value: $integer" + return 0 + } + $result = $result -bor $integer + } + + if ([String]::IsNullOrEmpty($result)) + { + return 0 + } + + return $result +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Group-ACEs.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Group-ACEs.ps1 new file mode 100644 index 000000000..7237e2914 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Group-ACEs.ps1 @@ -0,0 +1,90 @@ +<# +.SYNOPSIS + Groups Access Control Entries (ACEs) by identity and permissions. + +.DESCRIPTION + The Group-ACEs function takes an array of ACE objects and groups them by their identity. + It then further groups the identities by their permissions. The function returns a list + of grouped ACEs. + +.PARAMETER ACEs + An array of ACE objects to be grouped. Each ACE object should have an Identity and Permissions property. + +.OUTPUTS + System.Collections.Generic.List[HashTable] + A list of hash tables where each hash table represents a grouped ACE with its identity and permissions. + +.EXAMPLE + $aces = Get-ACEs + $groupedACEs = Group-ACEs -ACEs $aces + Write-Output $groupedACEs + +.NOTES + The function uses verbose output to provide detailed information about its processing steps. +#> +Function Group-ACEs +{ + param( + # Mandatory parameter: an array of ACE objects. + [Parameter()] + [Object[]]$ACEs + ) + + Write-Verbose "[Group-ACE] Started." + + # Check if the ACEs are not found. + if (-not $ACEs) + { + Write-Verbose "[Group-ACE] ACEs not found." + return + } + + Write-Verbose "[Group-ACE] Initializing empty list to hold the ACEs." + + # Initialize an empty list to hold the ACEs (Access Control Entries). + $ACEList = [System.Collections.Generic.List[HashTable]]::new() + + Write-Verbose "[Group-ACE] Grouping the ACEs by identity." + + # Group the ACEs by the identity. + $GroupedIdentities = $ACEs | Group-Object -Property { $_.Identity.value.originId } + + Write-Verbose "[Group-ACE] Filtering groups based on count." + + # Filter by the count + $Single, $Multiple = $GroupedIdentities.Where({ $_.Count -eq 1 }, 'Split') + + Write-Verbose "[Group-ACE] Adding single identities to the ACE list." + + # Add the single identities to the ACE list + $Single | ForEach-Object { + Write-Verbose "[Group-ACE] Adding single identity: $($_.Group[0].Identity)" + $ACEList.Add($_.Group[0]) + } + + Write-Verbose "[Group-ACE] Grouping multiple identities by permissions." + + # Group the multiple identities by the permissions + ForEach ($item in $Multiple) + { + Write-Verbose "[Group-ACE] Processing multiple identity: $($item.Group[0].Identity)" + + # Create a new hash table for the group + $ht = @{ + Identity = $item.Group[0].Identity + Permissions = @{ + Deny = $item.Group.Permissions.Deny | Sort-Object -Unique Bit + Allow = $item.Group.Permissions.Allow | Sort-Object -Unique Bit + DescriptorType = $item.Group[0].Permissions.DescriptorType + } + } + + Write-Verbose "[Group-ACE] Adding grouped identity with permissions." + + $ACEList.Add($ht) + } + + Write-Verbose "[Group-ACE] Completed." + $ACEList + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/New-ACLToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/New-ACLToken.ps1 new file mode 100644 index 000000000..ac59d3d57 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/New-ACLToken.ps1 @@ -0,0 +1,120 @@ +<# +.SYNOPSIS +Converts a security TokenName to an ACL token based on the security namespace. + +.DESCRIPTION +The New-ACLToken function converts a security TokenName to an ACL token based on the specified security namespace. It is used in the Azure DevOps DSC module to derive the token type and other relevant information for Git repositories. + +.PARAMETER SecurityNamespace +Specifies the security namespace for which the ACL token needs to be generated. + +.PARAMETER TokenName +Specifies the security TokenName that needs to be converted to an ACL token. + +.OUTPUTS +The function returns a hashtable containing the following properties: +- type: The type of the ACL token (e.g., GitOrganization, GitProject, GitRepository, GitUnknown, UnknownSecurityNamespace). +- inherited: Indicates whether the security TokenName is inherited or not. +- projectId: The ID of the project associated with the ACL token (applicable for GitProject and GitRepository types). +- RepoId: The ID of the repository associated with the ACL token (applicable for GitRepository type). + +.EXAMPLE +New-ACLToken -SecurityNamespace 'Git Repositories' -TokenName 'Contoso/Org/Project' + +This example converts the security TokenName 'Contoso/Org/Project' to an ACL token for the 'Git Repositories' security namespace. The resulting ACL token will have the type 'GitProject' and the project ID will be retrieved from the cache. + +.NOTES +This function is part of the AzureDevOpsDsc.Common module and is used internally by other functions in the module. +#> + +Function New-ACLToken +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string]$SecurityNamespace, + + [Parameter(Mandatory = $true)] + [string]$TokenName + + ) + + $TokenName = $TokenName.Replace('[', '').Replace(']', '') + + Write-Verbose "[New-ACLToken] Started." + Write-Verbose "[New-ACLToken] Security Namespace: $SecurityNamespace" + Write-Verbose "[New-ACLToken] Token Name: $TokenName" + + $result = @{} + + # Create a new ACL Object + switch ($SecurityNamespace) + { + + # Git Repositories + 'Git Repositories' { + + # Derive the Token Type GitOrganization, GitProject or GitRepository + if ($TokenName -match $LocalizedDataAzResourceTokenPatten.OrganizationGit) + { + # Derive the Token Type GitOrganization + $result.type = 'GitOrganization' + } + elseif ($TokenName -match $LocalizedDataAzResourceTokenPatten.GitProject) + { + # Derive the Token Type GitProject + $result.type = 'GitProject' + $result.projectId = (Get-CacheItem -Key $matches.ProjectName.Trim() -Type 'LiveProjects').id + } + elseif ($TokenName -match $LocalizedDataAzResourceTokenPatten.GitRepository) + { + # Derive the Token Type GitRepository + $result.type = 'GitRepository' + $result.projectId = (Get-CacheItem -Key $matches.ProjectName.Trim() -Type 'LiveProjects').id + $result.RepoId = (Get-CacheItem -Key $TokenName -Type 'LiveRepositories').id + } + else + { + # Derive the Token Type GitUnknown + $result.type = 'GitUnknown' + Write-Warning "[New-ACLToken] TokenName '$TokenName' does not match any known Git ACL Token Patterns." + } + break; + } + + # Identity + 'Identity' { + + # Derive the Token Type Identity + if ($TokenName -match $LocalizedDataAzResourceTokenPatten.GroupPermission) + { + # Derive the Token Type Identity + $result.type = 'GitGroupPermission' + $result.projectId = $matches.ProjectId + $result.groupId = $matches.GroupId + } + else + { + # Derive the Token Type Identity + $result.type = 'GroupUnknown' + Write-Warning "[New-ACLToken] TokenName '$TokenName' does not match any known Identity ACL Token Patterns." + } + + $result.type = 'Identity' + break; + } + + default { + Write-Warning "[New-ACLToken] SecurityNamespace '$SecurityNamespace' is not recognized." + $result.type = 'UnknownSecurityNamespace' + } + + } + + Write-Verbose "[New-ACLToken] ACL Token: $($result | Out-String)" + + # Return the ACL Token + return $result + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Parse-ACLToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Parse-ACLToken.ps1 new file mode 100644 index 000000000..d63f4c8a5 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Parse-ACLToken.ps1 @@ -0,0 +1,84 @@ +Function Parse-ACLToken +{ + param( + [Parameter(Mandatory = $true)] + [String]$Token, + + [Parameter(Mandatory = $true)] + [ValidateSet('Identity', 'Git Repositories')] + [String]$SecurityNamespace + ) + + $result = @{} + + Write-Verbose "[Parse-ACLToken] Started." + Write-Verbose "[Parse-ACLToken] Token: $Token" + Write-Verbose "[Parse-ACLToken] Security Namespace: $SecurityNamespace" + + # + # Git Repositories + if ($SecurityNamespace -eq 'Git Repositories') + { + # Match the Token with the Regex Patterns + switch -regex ($Token.Trim()) + { + $LocalizedDataAzACLTokenPatten.OrganizationGit { + $result.type = 'OrganizationGit' + break; + } + + $LocalizedDataAzACLTokenPatten.GitProject { + $result.type = 'GitProject' + break; + } + + $LocalizedDataAzACLTokenPatten.GitRepository { + $result.type = 'GitRepository' + break; + } + + $LocalizedDataAzACLTokenPatten.GitBranch { + $result.type = 'GitBranch' + break; + } + + default { + throw "Token '$Token' is not recognized." + } + } + + # + # Identity + } + elseif ($SecurityNamespace -eq 'Identity') + { + + # Match the Token with the Regex Patterns + switch -regex ($Token.Trim()) + { + + $LocalizedDataAzACLTokenPatten.ResourcePermission { + $result.type = 'ResourcePermission' + break; + } + + $LocalizedDataAzACLTokenPatten.GroupPermission { + $result.type = 'GroupPermission' + break; + } + + default { + throw "Token '$Token' is not recognized." + } + } + } + + # Get all Capture Groups and add them into a hashtable + $matches.keys | Where-Object { $_.Length -gt 1 } | ForEach-Object { + $result."$_" = $matches."$_" + } + + $result._token = $Token + + return $result +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Resolve-ACLToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Resolve-ACLToken.ps1 new file mode 100644 index 000000000..14534e055 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Resolve-ACLToken.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Resolves the ACL token from the provided reference or difference ACL objects. + +.DESCRIPTION + The Resolve-ACLToken function returns the token from the provided ACL objects. + It prefers the token from the DifferenceObject if it is not null, as it contains + the most recent information. If the DifferenceObject is null, it returns the token + from the ReferenceObject. + +.PARAMETER ReferenceObject + The reference ACL object(s) from which the token can be resolved if the DifferenceObject is null. + +.PARAMETER DifferenceObject + The difference ACL object(s) from which the token is preferred if it is not null. + +.EXAMPLE + $referenceACL = @{} + $differenceACL = @{} + $token = Resolve-ACLToken -ReferenceObject $referenceACL -DifferenceObject $differenceACL + +.NOTES + This function is part of the AzureDevOpsDsc module. +#> +function Resolve-ACLToken +{ + param ( + # Reference ACL + [Parameter()] + [Object[]] + $ReferenceObject, + + # Difference ACL + [Parameter()] + [Object[]] + $DifferenceObject + ) + + Write-Verbose "[Resolve-ACLToken] Started." + + # Prefer the Difference ACL if it is not null. This is because the Difference ACL contains the most recent information. + if ($null -ne $DifferenceObject) + { + Write-Verbose "[Resolve-ACLToken] Difference ACL is not null." + return $DifferenceObject.token._token + } + + Write-Verbose "[Resolve-ACLToken] Difference ACL is null." + return $ReferenceObject.token._token +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Test-ACLListforChanges.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Test-ACLListforChanges.ps1 new file mode 100644 index 000000000..a3f734be1 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Test-ACLListforChanges.ps1 @@ -0,0 +1,233 @@ + +# The Azure Devops ACL API is different to other ACL APIs where it only provides a means to get, remove and set ACLs. +# This means that if there is a change to the ACL's then the entire ACL must be set again. +# This function captures the differences between two ACLs and if there is a different the properties changed will contain the new reference ACL. +# + +Function Test-ACLListforChanges +{ + [CmdletBinding()] + param ( + # The Reference ACL to compare against. + [Parameter()] + [Object[]] + $ReferenceACLs, + + # The Difference ACL to compare against. + [Parameter()] + [Object[]] + $DifferenceACLs + ) + + Write-Verbose "[Test-ACLListforChanges] Started." + + $result = @{ + status = "Unchanged" + reason = @( + @{ + Value = "No changes detected." + Reason = "No changes detected." + } + ) + propertiesChanged = @() + } + + # + # Test if the Reference and Difference ACLs are null. + + if (($ReferenceACLs.aces -eq $null) -and ($DifferenceACLs -eq $null)) + { + Write-Verbose "[Test-ACLListforChanges] ACLs are null." + return $result + } + + # Get the Token + #$Token = Get-ACLToken $ReferenceACLs $DifferenceACLs + + # If the Reference ACL is null, set the status to changed. + if (($null -eq $ReferenceACLs) -or ($null -eq $ReferenceACLs.aces)) + { + Write-Verbose "[Test-ACLListforChanges] Reference ACL is null." + $result.status = "Missing" + $result.propertiesChanged = $DifferenceACLs + $result.reason += @{ + Value = $DifferenceACLs + Reason = "Reference ACL is null." + } + return $result + } + + # If the Difference ACL is null, set the status to changed. + if ($null -eq $DifferenceACLs) + { + Write-Verbose "[Test-ACLListforChanges] Difference ACL is null." + $result.status = "NotFound" + $result.propertiesChanged = $ReferenceACLs + $result.reason += @{ + Value = $ReferenceACLs + Reason = "Difference ACL is null." + } + return $result + } + + # Set the flag to be false + $isChanged = $false + + # + # Test if the Reference and Difference ACLs count is not equal. + + if ($ReferenceACLs.ACEs.Count -ne $DifferenceACLs.ACEs.Count) + { + Write-Verbose "[Test-ACLListforChanges] ACLs count is not equal." + $result.status = "Changed" + $result.reason += @{ + Value = $ReferenceACLs + Reason = "ACLs count is not equal." + } + $result.propertiesChanged = $ReferenceACLs + return $result + } + + # + # Test if the inherited flag is not equal. + + if ($ReferenceACLs.inherited -ne $DifferenceACLs.inherited) + { + Write-Verbose "[Test-ACLListforChanges] Inherited flag is not equal." + $result.status = "Changed" + $result.reason += @{ + Value = $ReferenceACLs + Reason = "Inherited flag is not equal." + } + $result.propertiesChanged = $ReferenceACLs + return $result + } + + # + # Test each of the Reference ACLs + ForEach ($ReferenceACL in $ReferenceACLs) + { + + $acl = $DifferenceACLs | Where-Object { $_.Identity.value.originId -eq $ReferenceACL.Identity.value.originId } + + # Test if the ACL is not found in the Difference ACL. + if ($null -eq $acl) + { + $result.status = "Changed" + $result.propertiesChanged = $ReferenceACLs + $result.reason += @{ + Value = $ReferenceACL + Reason = "ACL not found in Difference ACL." + } + return $result + } + + # Test the inherited flag. + if ($ReferenceACL.isInherited -ne $acl.isInherited) + { + $result.status = "Changed" + $result.propertiesChanged = $ReferenceACLs + $result.reason += @{ + Value = $ReferenceACL + Reason = "Inherited flag is not equal." + } + return $result + } + + # Iterate through the ACEs and compare them. + + ForEach ($ReferenceACE in $ReferenceACL.ACEs) + { + + # Check if the ACE is found in the Difference ACL. + $ace = $DifferenceACLs.ACEs | Where-Object { $_.Identity.value.originId -eq $ReferenceACE.Identity.value.originId } + + # Check if the ACE is not found in the Difference ACL. + if ($null -eq $ace) + { + $result.status = "Changed" + $result.propertiesChanged = $ReferenceACLs + $result.reason += @{ + Value = $ReferenceACE + Reason = "ACE not found in Difference ACL." + } + return $result + } + + # + # From this point on, we know that the ACE is found in both ACLs. + + # + # Compare the Allow ACEs + + $ReferenceAllow = Get-BitwiseOrResult $ReferenceACE.Permissions.Allow.Bit + $DifferenceAllow = Get-BitwiseOrResult $ace.Permissions.Allow.Bit + + # Test if the integers are not equal. + if ($ReferenceAllow -ne $DifferenceAllow) + { + Write-Verbose "[Test-ACLListforChanges] Allow ACEs are not equal." + $result.propertiesChanged = $ReferenceACLs + $result.reason += @{ + Value = @{ + ReferenceAllow = $ReferenceAllow + DifferenceAllow = $DifferenceAllow + } + Reason = "Allow ACEs are not equal." + } + $result.status = "Changed" + } + + # + # Compare the Deny ACEs + + $ReferenceDeny = Get-BitwiseOrResult $ReferenceACE.Permissions.Deny.Bit + $DifferenceDeny = Get-BitwiseOrResult $ace.Permissions.Deny.Bit + + # Test if the integers are not equal. + if ($ReferenceDeny -ne $DifferenceDeny) + { + Write-Verbose "[Test-ACLListforChanges] Deny ACEs are not equal." + $result.propertiesChanged = $ReferenceACLs + $result.reason += @{ + Value = @{ + ReferenceDeny = $ReferenceDeny + DifferenceDeny = $DifferenceDeny + } + Reason = "Deny ACEs are not equal." + } + $result.status = "Changed" + } + + } + + } + + # + # Test each of the Difference ACLs + + foreach ($DifferenceACL in $DifferenceACLs) + { + + $acl = $ReferenceACLs | Where-Object { $_.Identity.value.originId -eq $DifferenceACL.Identity.value.originId } + + # Test if the ACL is not found in the Reference ACL. + if ($null -eq $acl) + { + $result.status = "Changed" + $result.reason += @{ + Value = $DifferenceACL + Reason = "ACL not found in Reference ACL." + } + $result.propertiesChanged = $ReferenceACLs + return $result + } + + # No other tests are required as the Reference ACL has already been tested. + + } + + # Result the result hash table. + return $result + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiHttpRequestHeader.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiHttpRequestHeader.ps1 similarity index 59% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiHttpRequestHeader.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiHttpRequestHeader.ps1 index f52ff871b..3d8d4fe24 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiHttpRequestHeader.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiHttpRequestHeader.ps1 @@ -5,6 +5,8 @@ NOTE: Use of the '-IsValid' switch is required. + PAT Tokens and Managed Identity Tokens are allowed. + .PARAMETER HttpRequestHeader The 'HttpRequestHeader' to be tested/validated. @@ -26,17 +28,39 @@ function Test-AzDevOpsApiHttpRequestHeader [OutputType([System.Boolean])] param ( - [Parameter(Mandatory = $true)] + [Parameter()] [Hashtable] $HttpRequestHeader, - [Parameter(Mandatory = $true)] + [Parameter()] [ValidateSet($true)] [System.Management.Automation.SwitchParameter] $IsValid ) - return !($null -eq $HttpRequestHeader -or - $null -eq $HttpRequestHeader.Authorization -or - $HttpRequestHeader.Authorization -inotlike 'Basic *') + # if Metadata is specifed within the header then it is a Managed Identity Token request + # and is valid. + + if ($HttpRequestHeader.Metadata) + { + return $true + } + + # Otherwise, if the header is not valid, retrun false + if ($null -eq $HttpRequestHeader) + { + return $false + } + + # If the header is not null, but the Authorization is null, return false + if ($HttpRequestHeader.Authorization) + { + if ($HttpRequestHeader.Authorization -match '^(Basic|Bearer)(:\s|\s).+$') + { + return $true + } + } + + return $false + } diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceId.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiResourceId.ps1 similarity index 96% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceId.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiResourceId.ps1 index c585b21cc..f4ea494e1 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceId.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiResourceId.ps1 @@ -30,12 +30,12 @@ function Test-AzDevOpsApiResourceId [System.String] $ResourceId, - [Parameter(Mandatory = $true)] + [Parameter()] [ValidateSet($true)] [System.Management.Automation.SwitchParameter] $IsValid ) - return !(![guid]::TryParse($ResourceId, $([ref][guid]::Empty))) + } diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiTimeoutExceeded.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiTimeoutExceeded.ps1 similarity index 99% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiTimeoutExceeded.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiTimeoutExceeded.ps1 index 998dcfeaf..c435b16b5 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiTimeoutExceeded.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiTimeoutExceeded.ps1 @@ -48,4 +48,5 @@ function Test-AzDevOpsApiTimeoutExceeded ) return $($(New-TimeSpan -Start $StartTime -End $EndTime).TotalMilliseconds -gt $TimeoutMs) + } diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiUri.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiUri.ps1 similarity index 84% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiUri.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiUri.ps1 index 9378da005..8912e8247 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiUri.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiUri.ps1 @@ -26,16 +26,22 @@ function Test-AzDevOpsApiUri [OutputType([System.Boolean])] param ( - [Parameter(Mandatory = $true)] + [Parameter()] [System.String] $ApiUri, - [Parameter(Mandatory = $true)] + [Parameter()] [ValidateSet($true)] [System.Management.Automation.SwitchParameter] $IsValid ) + # The APIUri is not mandatory. If it is not provided, the function will return $true. + if ([String]::IsNullOrEmpty($ApiUri)) + { + return $true + } + return !(($ApiUri -inotlike 'http://*' -and $ApiUri -inotlike 'https://*') -or $ApiUri -inotlike '*/_apis/') diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiVersion.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiVersion.ps1 similarity index 94% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiVersion.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiVersion.ps1 index fc30e8857..91f588d29 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiVersion.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiVersion.ps1 @@ -27,11 +27,11 @@ function Test-AzDevOpsApiVersion [OutputType([System.Boolean])] param ( - [Parameter(Mandatory = $true)] + [Parameter()] [System.String] $ApiVersion, - [Parameter(Mandatory = $true)] + [Parameter()] [ValidateSet($true)] [System.Management.Automation.SwitchParameter] $IsValid diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Wait-AzDevOpsApiResource.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Wait-AzDevOpsApiResource.ps1 similarity index 94% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Wait-AzDevOpsApiResource.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Wait-AzDevOpsApiResource.ps1 index eb5de524f..97033e047 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Wait-AzDevOpsApiResource.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Wait-AzDevOpsApiResource.ps1 @@ -119,8 +119,10 @@ function Wait-AzDevOpsApiResource # Wait/Sleep while... # 1) Resource is currently absent but waiting for presence or; # 2) Resource is currently present but waiting for absence - while (($IsPresent -and -not $testAzDevOpsApiResource) -or - ($IsAbsent -and $testAzDevOpsApiResource)) + while ( + ($IsPresent -and -not $testAzDevOpsApiResource) -or + ($IsAbsent -and $testAzDevOpsApiResource) + ) { Start-Sleep -Milliseconds $WaitIntervalMilliseconds @@ -130,8 +132,14 @@ function Wait-AzDevOpsApiResource New-InvalidOperationException -Message $errorMessage } - [bool]$testAzDevOpsApiResource = Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat ` - -ResourceName $ResourceName ` - -ResourceId $ResourceId + $params = @{ + ApiUri = $ApiUri + Pat = $Pat + ResourceName = $ResourceName + ResourceId = $ResourceId + } + + [bool]$testAzDevOpsApiResource = Test-AzDevOpsApiResource @params + } } diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ConvertTo-Base64String.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ConvertTo-Base64String.ps1 new file mode 100644 index 000000000..b55aed41a --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ConvertTo-Base64String.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS +Converts a byte array to a Base64 string. + +.DESCRIPTION +The ConvertTo-Base64String function takes a byte array as input and converts it to a Base64 string representation. + +.PARAMETER InputObject +The byte array to be converted to a Base64 string. + +.EXAMPLE +$bytes = [System.Text.Encoding]::UTF8.GetBytes("Hello, World!") +$base64String = ConvertTo-Base64String -InputObject $bytes +$base64String +# Output: SGVsbG8sIFdvcmxkIQ== + +.NOTES +Author: GitHub Copilot +Date: 2025-01-06 +#> +function ConvertTo-Base64String +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [String] + $InputObject + ) + + process + { + [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($InputObject)) + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-AzDoIdentity.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-AzDoIdentity.ps1 new file mode 100644 index 000000000..cfa3ca5d1 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-AzDoIdentity.ps1 @@ -0,0 +1,157 @@ +<# +.SYNOPSIS + Finds an Azure DevOps identity (user or group) based on the provided identity string. + +.DESCRIPTION + The Find-AzDoIdentity function searches for an Azure DevOps identity (user or group) using the provided identity string. + The identity string can be an email address, a group name, or a display name. The function first checks if the identity + is an email address or a group name and performs the lookup accordingly. If the identity is neither, it performs a lookup + using the display name. + +.PARAMETER Identity + The identity string to search for. This can be an email address, a group name, or a display name. + +.EXAMPLE + PS> Find-AzDoIdentity -Identity "user@domain.com" + Finds the user with the specified email address. + +.EXAMPLE + PS> Find-AzDoIdentity -Identity "Project\GroupName" + Finds the group with the specified name. + +.EXAMPLE + PS> Find-AzDoIdentity -Identity "Display Name" + Finds the user or group with the specified display name. + +.NOTES + The function uses cached user and group data stored in the global variables $Global:AZDOLiveUsers and $Global:AZDOLiveGroups. + If multiple users or groups are found with the same display name, a warning is issued and no result is returned. + If both a user and a group are found with the same display name, a warning is issued and no result is returned. + +#> +Function Find-AzDoIdentity +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$Identity + ) + + Write-Verbose "[Find-AzDoIdentity] Starting identity lookup for '$Identity'." + + # Get the Usernames from the cache + $CachedUsers = $Global:AZDOLiveUsers + Write-Verbose "[Find-AzDoIdentity] Retrieved cached users." + + # Get the Groups from the Cache + $CachedGroups = $Global:AZDOLiveGroups + Write-Verbose "[Find-AzDoIdentity] Retrieved cached groups." + + switch ($Identity) + { + + # Test if the Username contains an '@' symbol. If it does, it is an email address and should be converted to a UPN + { $Identity -like '*@*' } { + Write-Verbose "[Find-AzDoIdentity] Identity is an email address; converting to UPN." + + # Perform a lookup using the existing username + $cachedItem = Get-CacheItem -Key $Identity -Type 'LiveUsers' + #$cachedItem = $cachedItem | Select-Object *, @{Name = 'Type'; Exp={'Users'}} + + # Test if the user is found + if ($null -eq $cachedItem) + { + Write-Warning "[Find-AzDoIdentity] No user found with the UPN '$Identity'." + return + } + + Write-Verbose "[Find-AzDoIdentity] Found user with UPN '$Identity'." + return $cachedItem + } + + # Test if the Username contains a '\' or '/' symbol. If it does, it is a group and needs to be sanitized + { $Identity -match '\\|\/' } { + Write-Verbose "[Find-AzDoIdentity] Identity contains a '\' or '/'; treated as a group." + + # If the Identity 'Project\GroupName' does not contain square brackets, add them around the Project. + if ($Identity -notmatch '\[.*\]') + { + $split = $Identity -split ('\\|\/') + $Identity = '[{0}]\{1}' -f $split[0], $split[1] + } + + # Perform a lookup using the existing username + Write-Verbose "[Find-AzDoIdentity] Performing a lookup using the group name '$Identity'." + $cachedItem = Get-CacheItem -Key $Identity -Type 'LiveGroups' + + # Test if the group is found + if ($null -eq $cachedItem) + { + Write-Warning "[Find-AzDoIdentity] No group found with the name '$Identity'." + return + } + + #$cachedItem = $cachedItem | Select-Object *, @{Name = 'Type'; Exp={'Group'}} + + Write-Verbose "[Find-AzDoIdentity] cachedItem '$($cachedItem | ConvertTo-Json)'." + Write-Verbose "[Find-AzDoIdentity] Found group with name '$Identity'." + + return $cachedItem + } + + # If all else fails, try and perform a lookup using the display name. + # If multiple users are found, throw an error. + # If no users are found, throw an error. + default { + + Write-Verbose "[Find-AzDoIdentity] Performing a lookup using the display name '$Identity'." + + # Perform a lookup using the existing username + [Array]$User = $CachedUsers | Where-Object { $_.value.displayName -eq $Identity } + [Array]$Group = $CachedGroups | Where-Object { $_.value.displayName -eq $Identity } + + # Write the number of users and groups found + Write-Verbose "[Find-AzDoIdentity] Found $($User.Count) users and $($Group.Count) groups with the display name '$Identity'." + + # Test if the user is found + if ($User.Count -gt 1) + { + Write-Warning "[Find-AzDoIdentity] Multiple users found with the display name '$Identity'. Please use the UPN (user@domain.com)" + return + } + + # Test if a group is found + if ($Group.Count -gt 1) + { + Write-Warning "[Find-AzDoIdentity] Multiple groups found with the display name '$Identity'. Please use the UPN ([project]\[groupname])" + return + } + + # Test if both a user and a group are found + if ($User.Count -eq 1 -and $Group.Count -eq 1) + { + Write-Warning "[Find-AzDoIdentity] Both a user and a group found with the display name '$Identity'. Please use the UPN (user@domain.com or [project]\[groupname])" + return + } + + if ($User.Count -eq 1) + { + # If the user is found, add the type and return the user + Write-Verbose "[Find-AzDoIdentity] Single user found with the display name '$Identity'." + return $User.value + } + elseif ($Group.Count -eq 1) + { + # If the group is found, add the type and return the group + Write-Verbose "[Find-AzDoIdentity] Single group found with the display name '$Identity'." + return $Group.value + } + + Write-Warning "No identity found for '$Identity'." + return + + } + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-Identity.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-Identity.ps1 new file mode 100644 index 000000000..ddd94ecac --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-Identity.ps1 @@ -0,0 +1,222 @@ +<# +.SYNOPSIS + Finds an identity (user or group) based on the provided name. + +.DESCRIPTION + The Find-Identity function searches for an identity (user or group) based on the provided name. + It first checks the cached groups and users to find a match. + If multiple identities with the same name are found, a warning is issued and null is returned. + +.PARAMETER Name + The name of the identity to search for. + +.PARAMETER OrganizationName + The name of the organization. + +.PARAMETER SearchType + The type of search to perform. Valid values are 'descriptor', 'descriptorId', 'displayName', 'originId', 'key'. + +.OUTPUTS + Returns the ACLIdentity object of the found identity. If no identity is found, null is returned. + +.NOTES + Author: Michael Zanatta + Date: 2025-01-06 + +.EXAMPLE + Find-Identity -Name "JohnDoe" + Returns the ACLIdentity object of the identity with the name "JohnDoe" if found. Otherwise, returns null. +#> + +Function Find-Identity +{ + [CmdletBinding()] + param( + # The name of the identity to search for. + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + + # The name of the organization. + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$OrganizationName, + + # The type of search to perform. + [Parameter()] + [ValidateSet('descriptor', 'descriptorId', 'displayName', 'originId', 'principalName')] + [string]$SearchType = 'descriptor' + ) + + # Logging + Write-Verbose "[Find-Identity] Started." + Write-Verbose "[Find-Identity] Name: $Name" + Write-Verbose "[Find-Identity] Organization Name: $OrganizationName" + Write-Verbose "[Find-Identity] Search Type: $SearchType" + + try + { + $CachedGroups = Get-CacheObject -CacheType 'LiveGroups' + $CachedUsers = Get-CacheObject -CacheType 'LiveUsers' + $CachedServicePrincipals = Get-CacheObject -CacheType 'LiveServicePrinciples' + } + catch + { + Write-Error "Failed to retrieve cache objects: $_" + return $null + } + + # + # Define the lookup table based on the search type + switch ($SearchType) + { + 'descriptor' { + $lookup = @{ + groupIdentitySB = { $_.value.ACLIdentity.descriptor -eq $Name } + userIdentitySB = { $_.value.ACLIdentity.descriptor -eq $Name } + servicePrincipalIdentitySB = { $_.value.ACLIdentity.descriptor -eq $Name } + } + } + 'descriptorId' { + $lookup = @{ + groupIdentitySB = { $_.value.ACLIdentity.id -eq $Name } + userIdentitySB = { $_.value.ACLIdentity.id -eq $Name } + servicePrincipalIdentitySB = { $_.value.ACLIdentity.id -eq $Name } + } + } + 'originId' { + $lookup = @{ + groupIdentitySB = { $_.value.originId -eq $Name } + userIdentitySB = { $_.value.originId -eq $Name } + servicePrincipalIdentitySB = { $_.value.originId -eq $Name } + } + } + 'principalName' { + $lookup = @{ + groupIdentitySB = { $_.value.principalName.replace('[','').replace(']','') -eq $Name } + userIdentitySB = { $_.value.principalName -eq $Name } + servicePrincipalIdentitySB = { $_.value.principalName -eq $Name } + } + } + 'displayName' { + $lookup = @{ + groupIdentitySB = { $_.value.displayName -eq $Name } + userIdentitySB = { $_.value.displayName -eq $Name } + servicePrincipalIdentitySB = { $_.value.displayName -eq $Name } + } + } + default { + Write-Error "Invalid SearchType: $SearchType" + return $null + } + } + + # + # Find the identity + + $groupIdentity = $CachedGroups | Where-Object $lookup.groupIdentitySB + $userIdentity = $CachedUsers | Where-Object $lookup.userIdentitySB + $servicePrincipalIdentity = $CachedServicePrincipals | Where-Object $lookup.servicePrincipalIdentitySB + + # Check if multiple identities were found. + if ($groupIdentity -or $userIdentity -or $servicePrincipalIdentity) + { + + if ( + ($groupIdentity -and $userIdentity) -or + ($groupIdentity -and $servicePrincipalIdentity) -or + ($userIdentity -and $servicePrincipalIdentity) + ) + { + Write-Warning "[Find-Identity] Found multiple identities with the name '$Name'. Returning null." + return $null + } + + if ($groupIdentity) + { + Write-Verbose "[Find-Identity] Found group identity for '$Name'." + Write-Verbose "[Find-Identity] $SearchType" + return $groupIdentity + } + elseif ($userIdentity) + { + Write-Verbose "[Find-Identity] Found user identity for '$Name'." + return $userIdentity + } + elseif ($servicePrincipalIdentity) + { + Write-Verbose "[Find-Identity] Found service principal identity for '$Name'." + return $servicePrincipalIdentity + } + + } + else + { + + Write-Warning "[Find-Identity] No identity found for '$Name'. Performing a lookup via the API." + + # Perform a lookup via the API + $params = @{ + OrganizationName = $OrganizationName + Descriptor = $Name + } + + Write-Verbose "[Find-Identity] Performing a lookup via the API." + Write-Verbose "[Find-Identity] $SearchType" + + try + { + # Get the identity + $identity = Get-DevOpsDescriptorIdentity @params + } + catch + { + Write-Error "Failed to retrieve identity via API: $_" + return $null + } + + # Attempt to match the identity using the ID + $groupIdentity = $CachedGroups | Where-Object { $_.value.ACLIdentity.id -eq $identity.id } + $userIdentity = $CachedUsers | Where-Object { $_.value.ACLIdentity.id -eq $identity.id } + $servicePrincipalIdentity = $CachedServicePrincipals | Where-Object { $_.value.ACLIdentity.id -eq $identity.id } + + # Test if the identity was found + if ($groupIdentity -or $userIdentity -or $servicePrincipalIdentity) + { + # Check if multiple identities were found. + if ( + ($groupIdentity -and $userIdentity) -or + ($groupIdentity -and $servicePrincipalIdentity) -or + ($userIdentity -and $servicePrincipalIdentity) + ) + { + Write-Warning "[Find-Identity] Found multiple identities with the ID '$($identity.id)'. Returning null." + return $null + } + + if ($groupIdentity) + { + Write-Verbose "[Find-Identity] Found group identity for '$Name'." + return $groupIdentity + } + elseif ($userIdentity) + { + Write-Verbose "[Find-Identity] Found user identity for '$Name'." + return $userIdentity + } + elseif ($servicePrincipalIdentity) + { + Write-Verbose "[Find-Identity] Found service principal identity for '$Name'." + return $servicePrincipalIdentity + } + } + + # If no identity was found, write a warning and return null + Write-Warning "[Find-Identity] No identity found for '$Name'." + return $null + + } + + # Return null if no identity was found + return $null +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroup.ps1 new file mode 100644 index 000000000..a1a37aefa --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroup.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS +Formats a user principal name by combining a prefix and a group name. + +.DESCRIPTION +The Format-AzDoGroup function takes a prefix and a group name as input parameters and returns a formatted user principal name. The user principal name is formatted as "[Prefix]\[GroupName]". + +.PARAMETER Prefix +The prefix to be used in the user principal name. + +.PARAMETER GroupName +The group name to be used in the user principal name. + +.EXAMPLE +Format-AzDoGroup -Prefix "Contoso" -GroupName "Developers" +Returns: "[Contoso]\Developers" + +#> + +Function Format-AzDoGroup +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Alias('ProjectName', 'Organization')] + [string] + $Prefix, + + [Parameter(Mandatory = $true)] + [String] + $GroupName + ) + + # If the prefix contains starting or ending square brackets, remove them. + $Prefix = $Prefix -replace '^\[|\]$', '' + + # Build the User Principal Name string + $userPrincipalName = '[{0}]\{1}' -f $Prefix, $GroupName + + # Use a verbose statement to show the input and resulting formatted UPN + Write-Verbose "[Format-AzDoGroup] Formatting User Principal Name with Prefix: '$Prefix' and GroupName: '$GroupName'." + Write-Verbose "[Format-AzDoGroup] Resulting User Principal Name: '$userPrincipalName'." + + return $userPrincipalName + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroupMember.ps1 new file mode 100644 index 000000000..8df52222f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroupMember.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS +Formats an Azure DevOps group member name by removing square brackets and reformatting the string. + +.PARAMETER GroupName +The name of the group to be formatted. This parameter is mandatory. + +.RETURNS +System.String +A formatted string representing the group name in the format '[prefix]\group'. + +.EXAMPLE +$formattedName = Format-AzDoGroupMember -GroupName '[prefix]\group' +# This will return 'prefix\group'. + +#> +Function Format-AzDoGroupMember +{ + param( + [Parameter(Mandatory = $true)] + [System.String]$GroupName + ) + + # If the group name contains starting or ending square brackets, remove them. + $GroupName = $GroupName -replace '^\[|\]', '' + + # Build the GroupName string + + # Split the GroupName into the prefix and the group name. + $prefix, $group = $GroupName -split '\\' + + return '[{0}]\{1}' -f $prefix, $group + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoProjectName.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoProjectName.ps1 new file mode 100644 index 000000000..5bcb86d5e --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoProjectName.ps1 @@ -0,0 +1,84 @@ +<# +.SYNOPSIS +Formats the Azure DevOps project name. + +.DESCRIPTION +This function formats the Azure DevOps project name by ensuring it follows the correct format. +If the group name is already in the format '[Project|Organization]\GroupName', it returns the group name as is. +Otherwise, it splits the group name and ensures it has the correct format. + +.PARAMETER GroupName +The name of the group. This parameter is mandatory. + +.PARAMETER OrganizationName +The name of the organization. This parameter is mandatory. + +.EXAMPLE +PS> Format-AzDoProjectName -GroupName 'Project\Group' -OrganizationName 'MyOrg' +[Project]\Group + +.EXAMPLE +PS> Format-AzDoProjectName -GroupName '%ORG%\Group' -OrganizationName 'MyOrg' +[MyOrg]\Group + +.EXAMPLE +PS> Format-AzDoProjectName -GroupName '%TFS%\Group' -OrganizationName 'MyOrg' +[TEAM FOUNDATION]\Group + +.NOTES +If the group name is not in the correct format, an error is thrown. +#> +Function Format-AzDoProjectName +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter(Mandatory = $true)] + [Alias('Organization')] + [System.String]$OrganizationName + ) + + # Logging + Write-Verbose "[Format-AzDoProjectName] Formatting GroupName." + + # If the GroupName contains [Project|Organization]\GroupName, it's in the correct format. + if ($GroupName -match '^\[.*\]\\.*$') + { + return $GroupName + } + + # Split the group name with a '\' or '/' and create an array. + $splitGroupName = $GroupName -split '\\|\/' + + # There must be at least 2 elements in the array. The first element is the project name and the second element is the group name. + if ($splitGroupName.Length -lt 2) + { + Throw "The GroupName '$GroupName' is not in the correct format. The GroupName must be in the format 'ProjectName\GroupName' or 'Project/GroupName." + } + + # If the first element contains '%ORG%' or is empty, replace it with the organization name. + if ($splitGroupName[0] -eq '%ORG%' -or [String]::IsNullOrEmpty($splitGroupName[0].Trim())) + { + $splitGroupName[0] = $OrganizationName + } + elseif ($splitGroupName[0].Trim() -eq '%TFS%') + { + # If the first element contains '%TFS%', replace it with 'TEAM FOUNDATION'. + $splitGroupName[0] = 'TEAM FOUNDATION' + } + + # The group name cannot be null. + if ([String]::IsNullOrEmpty($splitGroupName[1].Trim())) + { + Throw "The GroupName '$GroupName' is not in the correct format. The GroupName must be in the format 'ProjectName\GroupName'." + } + + # Format the group name with the organization name. + $formattedGroupName = '[{0}]\{1}' -f $splitGroupName[0].Trim(), $splitGroupName[1].Trim() + + return $formattedGroupName +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-DescriptorType.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-DescriptorType.ps1 new file mode 100644 index 000000000..d3ab71669 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-DescriptorType.ps1 @@ -0,0 +1,48 @@ +<# +.SYNOPSIS +Formats the descriptor type to match the expected naming conventions. + +.DESCRIPTION +This function takes a descriptor type as input and returns a formatted string. +If the descriptor type is "GitRepositories", it returns "Git Repositories". +For all other descriptor types, it returns the input descriptor type unchanged. + +.PARAMETER DescriptorType +The descriptor type to be formatted. This parameter is mandatory. + +.OUTPUTS +System.String +The formatted descriptor type. + +.EXAMPLE +PS C:\> Format-DescriptorType -DescriptorType "GitRepositories" +Git Repositories + +.EXAMPLE +PS C:\> Format-DescriptorType -DescriptorType "OtherType" +OtherType +#> +Function Format-DescriptorType +{ + [CmdletBinding()] + [OutputType([String])] + param ( + [Parameter(Mandatory = $true)] + [System.String]$DescriptorType + ) + + # Switch on the DescriptorType + switch ($DescriptorType) + { + + # The Descriptor Name in the API is different to the Descriptor Name in the DSC Resource. + "GitRepositories" { + return "Git Repositories" + } + + # All other else, keep the same descriptor type + default { + return $DescriptorType + } + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiHttpRequestHeader.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiHttpRequestHeader.ps1 similarity index 100% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiHttpRequestHeader.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiHttpRequestHeader.ps1 diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceName.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceName.ps1 similarity index 100% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceName.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceName.ps1 diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceUri.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceUri.ps1 similarity index 99% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceUri.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceUri.ps1 index afd7515ed..9025ebdcc 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceUri.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceUri.ps1 @@ -59,7 +59,6 @@ function Get-AzDevOpsApiResourceUri [string]$apiResourceUri = $ApiUri - # Obtain URI-specific names relating to $ResourceName [string]$apiUriResourceAreaName = Get-AzDevOpsApiUriAreaName -ResourceName $ResourceName [string]$apiUriResourceName = Get-AzDevOpsApiUriResourceName -ResourceName $ResourceName @@ -71,18 +70,15 @@ function Get-AzDevOpsApiResourceUri $apiResourceUri = $apiResourceUri + "$apiUriResourceAreaName/" } - # Append the URI-specific, 'ResourceName' of the 'Resource' onto the URI $apiResourceUri = $apiResourceUri + "$apiUriResourceName/" - # Append the identifier of the resource, if provided if (![System.String]::IsNullOrWhiteSpace($ResourceId)) { $apiResourceUri = $apiResourceUri + "$ResourceId/" } - # Append any parameters to the URI $apiResourceUriParameters = @{ "api-version" = $ApiVersion # Taken from input parameter @@ -93,11 +89,9 @@ function Get-AzDevOpsApiResourceUri $apiResourceUri = $apiResourceUri + '?' $apiResourceUriParameters.Keys | ForEach-Object { - $apiResourceUri = $apiResourceUri + '&' + $_ + '=' + $apiResourceUriParameters[$_] } $apiResourceUri = $apiResourceUri.Replace('/?&','?') # Tidy up the end of base URI where initial parameter begins - return $apiResourceUri } diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriAreaName.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriAreaName.ps1 similarity index 100% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriAreaName.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriAreaName.ps1 diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriResourceName.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriResourceName.ps1 similarity index 100% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriResourceName.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriResourceName.ps1 diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiVersion.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiVersion.ps1 similarity index 82% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiVersion.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiVersion.ps1 index 370c89943..58e832890 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiVersion.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiVersion.ps1 @@ -19,15 +19,18 @@ function Get-AzDevOpsApiVersion $Default ) - [string]$defaultApiVersion = '6.0' + [string]$defaultApiVersion = '7.0-preview.1' [string[]]$apiVersions = @( #'4.0', # Not supported #'5.0', # Not supported #'5.1', # Not supported - '6.0' - #'6.1', # Not supported + '6.0', + '7.0-preview.1', + '7.1-preview.1', + '7.1-preview.4', + '7.2-preview.4' ) diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitIntervalMs.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitIntervalMs.ps1 similarity index 100% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitIntervalMs.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitIntervalMs.ps1 diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitTimeoutMs.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitTimeoutMs.ps1 similarity index 100% rename from source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitTimeoutMs.ps1 rename to source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitTimeoutMs.ps1 diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDoCacheObjects.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDoCacheObjects.ps1 new file mode 100644 index 000000000..c50469b2b --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDoCacheObjects.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS +Retrieves a list of Azure DevOps cache object types. + +.DESCRIPTION +The Get-AzDoCacheObjects function returns an array of strings representing different types of cache objects used in Azure DevOps. + +.OUTPUTS +String[] +An array of strings representing the cache object types. + +.EXAMPLE +PS> Get-AzDoCacheObjects +This command retrieves the list of Azure DevOps cache object types. + +#> +function Get-AzDoCacheObjects +{ + return @( + 'Project', + 'Team', + 'Group', + 'SecurityDescriptor', + 'LiveGroups', + 'LiveProjects', + 'LiveUsers', + 'LiveGroupMembers', + 'LiveRepositories', + 'LiveServicePrinciples', + 'LiveACLList', + 'LiveProcesses', + 'SecurityNamespaces' + ) + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Invoke-AzDevOpsApiRestMethod.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Invoke-AzDevOpsApiRestMethod.ps1 new file mode 100644 index 000000000..011ab6108 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Invoke-AzDevOpsApiRestMethod.ps1 @@ -0,0 +1,262 @@ +<# + .SYNOPSIS + This is a light, generic, wrapper around 'Invoke-RestMethod' to handle + multiple retries and error/exception handling. + + This function makes no assumptions around the versions of the API used, the resource + being operated/actioned upon, the operation/method being performed, nor the content + of the HTTP headers and body. + + .PARAMETER ApiUri + The URI of the Azure DevOps API to be connected to. For example: + + https://dev.azure.com/someOrganizationName/_apis/ + + .PARAMETER HttpMethod + The HTTP method being used in the HTTP/REST request sent to the Azure DevOps API. + + .PARAMETER HttpHeaders + The headers for the HTTP/REST request sent to the Azure DevOps API. + + .PARAMETER HttpBody + The body for the HTTP/REST request sent to the Azure DevOps API. If performing a 'Post', + 'Put' or 'Patch' method/request, this will typically contain the JSON document of the resource. + + .PARAMETER RetryAttempts + The number of times the method/request will attempt to be resent/retried if unsuccessful on the + initial attempt. + + If any attempt is successful, the remaining attempts are ignored. + + .PARAMETER RetryIntervalMs + The interval (in Milliseconds) between retry attempts. + + .EXAMPLE + Invoke-AzDevOpsApiRestMethod -ApiUri 'YourApiUriHere' -HttpMethod 'Get' -HttpHeaders $YouHttpHeadersHashtableHere + + Submits a 'Get' request to the Azure DevOps API (relying on the 'ApiUri' value to determine what is being retrieved). + + .EXAMPLE + Invoke-AzDevOpsApiRestMethod -ApiUri 'YourApiUriHere' -HttpMethod 'Patch' -HttpHeaders $YourHttpHeadersHashtableHere ` + -HttpBody $YourHttpBodyHere -RetryAttempts 3 + + Submits a 'Patch' request to the Azure DevOps API with the supplied 'HttpBody' and will attempt to retry 3 times (4 in + total, including the intitial attempt) if unsuccessful. +#> +function Invoke-AzDevOpsApiRestMethod +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject])] + param + ( + [Parameter(Mandatory=$true)] + [Alias('Uri')] + [System.String] + $ApiUri, + + [Parameter(Mandatory=$true)] + [ValidateSet('Get','Post','Patch','Put','Delete')] + [System.String] + [Alias('Method')] + $HttpMethod, + + [Parameter()] + [ValidateScript( { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $_ -IsValid })] + [Hashtable] + [Alias('Headers','HttpRequestHeader')] + $HttpHeaders=@{}, + + [Parameter()] + [System.String] + [Alias('Body')] + $HttpBody, + + [Parameter()] + [System.String] + [Alias('ContentType')] + [ValidateSet('application/json','application/json-patch+json')] + $HttpContentType = 'application/json', + + [Parameter()] + [ValidateRange(0,5)] + [Int32] + $RetryAttempts = 5, + + [Parameter()] + [ValidateRange(250,10000)] + [Int32] + $RetryIntervalMs = 250, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), + + [Parameter()] + [Switch] + $NoAuthentication, + + [Parameter()] + [Switch] + $AzureArcAuthentication + + ) + + $invokeRestMethodParameters = @{ + Uri = $ApiUri + Method = $HttpMethod + Headers = $HttpHeaders + Body = $HttpBody + ContentType = $HttpContentType + ResponseHeadersVariable = 'responseHeaders' + } + + Write-Verbose -Message ('[Invoke-AzDevOpsApiRestMethod] Invoking the Azure DevOps API REST method {0}' -f $HttpMethod) + Write-Verbose -Message ('[Invoke-AzDevOpsApiRestMethod] API URI: {0}' -f $ApiUri) + + # Remove the 'Body' and 'ContentType' if not relevant to request + if ($HttpMethod -in $('Get','Delete')) + { + $invokeRestMethodParameters.Remove('Body') + $invokeRestMethodParameters.Remove('ContentType') + } + + # Intially set this value to -1, as the first attempt does not want to be classed as a "RetryAttempt" + $CurrentNoOfRetryAttempts = -1 + # Set the Continuation Token to be False + $isContinuationToken = $false + $results = [System.Collections.ArrayList]::new() + + while ($CurrentNoOfRetryAttempts -lt $RetryAttempts) + { + <# + Slow down the retry attempts if the API resource is close to being overwelmed + If there are any retry attempts, wait for the specified number of seconds before retrying + #> + + if (($null -ne $Global:DSCAZDO_APIRateLimit.xRateLimitRemaining) -and ($Global:DSCAZDO_APIRateLimit.retryAfter -ge 0)) + { + Write-Verbose -Message ('[Invoke-AzDevOpsApiRestMethod] Waiting for {0} seconds before retrying.' -f $Global:DSCAZDO_APIRateLimit.retryAfter) + Start-Sleep -Seconds $Global:DSCAZDO_APIRateLimit.retryAfter + } + + # If the API resouce is close to beig overwelmed, wait for the specified number of seconds before sending the request + if (($null -ne $Global:DSCAZDO_APIRateLimit.xRateLimitRemaining) -and ($Global:DSCAZDO_APIRateLimit.xRateLimitRemaining -le 50) -and ($Global:DSCAZDO_APIRateLimit.xRateLimitRemaining -ge 5)) + { + Write-Verbose -Message "[Invoke-AzDevOpsApiRestMethod] Resource is close to being overwelmed. Waiting for $RetryIntervalMs seconds before sending the request." + Start-Sleep -Milliseconds $RetryIntervalMs + } + # If the API resouce is overwelmed, wait for the specified number of seconds before sending the request + elseif (($null -ne $Global:DSCAZDO_APIRateLimit.xRateLimitRemaining) -and ($Global:DSCAZDO_APIRateLimit.xRateLimitRemaining -lt 5)) + { + Write-Verbose -Message ('[Invoke-AzDevOpsApiRestMethod] Resource is overwhelmed. Waiting for {0} seconds to reset the TSTUs.' -f $Global:DSCAZDO_APIRateLimit.xRateLimitReset) + Start-Sleep -Milliseconds $RetryIntervalMs + } + + # + # Invoke the REST method. Loop until the Continuation Token is False. + + Do + { + # + # Add the Authentication Header + + # If the 'NoAuthentication' switch is NOT PRESENT and the 'Authentication' header is empty, add the authentication header + if (([String]::IsNullOrEmpty($invokeRestMethodParameters.Headers.Authentication)) -and (-not $NoAuthentication.IsPresent)) + { + $invokeRestMethodParameters.Headers.Authorization = Add-AuthenticationHTTPHeader + } + + # + # Invoke the REST method + + try + { + # Invoke the REST method. If the 'Verbose' switch is present, set it to $false. + # This is to prevent the output from being displayed in the console. + $response = Invoke-RestMethod @invokeRestMethodParameters -Verbose:$false + + # Zero out the 'Authorization' header + $invokeRestMethodParameters.Headers.Authorization = $null + # Add the response to the results array + $null = $results.Add($response) + + # + # Test to see if there is no continuation token + + if ([String]::IsNullOrEmpty($responseHeaders.'x-ms-continuationtoken')) + { + # If not, set the continuation token to False + $isContinuationToken = $false + # Update the Rate Limit information + $Global:DSCAZDO_APIRateLimit = $null + + Write-Verbose "[Invoke-AzDevOpsApiRestMethod] No continuation token found. Breaking loop." + + return $results + + } + + # + # A continuation token is present. + + # If so, set the continuation token to True + $isContinuationToken = $true + # Update the URI to include the continuation token + $invokeRestMethodParameters.Uri = '{0}&continuationToken={1}&{2}' -f $ApiUri, $responseHeaders.'x-ms-continuationtoken', $ApiVersion + # Reset the RetryAttempts counter + $CurrentNoOfRetryAttempts = -1 + + } + catch + { + + # If AzureArcAuthentication is present, then we need to handle the error differently. + # Stop and Pass the error back to the caller. The caller will handle the error. + if ($AzureArcAuthentication.IsPresent) + { + throw $_ + } + + # Zero out the 'Authorization' header + $invokeRestMethodParameters.Headers.Authorization = $null + # Check to see if it is an HTTP 429 (Too Many Requests) error + if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::TooManyRequests) + { + # If so, wait for the specified number of seconds before retrying + $retryAfter = $_.Exception.Response.Headers['Retry-After'] + + if ($retryAfter) + { + $retryAfter = [int]$retryAfter + Write-Verbose -Message "Received a 'Too Many Requests' response from the Azure DevOps API. Waiting for $retryAfter seconds before retrying." + $Global:DSCAZDO_APIRateLimit = [APIRateLimit]::New($retryAfter) + } + else + { + # If the Retry-After header is not present, wait for the specified number of milliseconds before retrying + Write-Verbose -Message "Received a 'Too Many Requests' response from the Azure DevOps API. Waiting for $RetryIntervalMs milliseconds before retrying." + $Global:DSCAZDO_APIRateLimit = [APIRateLimit]::New($RetryIntervalMs) + } + + } + + # Increment the number of retries attempted and obtain any exception message + $CurrentNoOfRetryAttempts++ + $restMethodExceptionMessage = $_.Exception.Message + + # Wait before the next attempt/retry + Start-Sleep -Milliseconds $RetryIntervalMs + + # Break the continuation token loop so that the next attempt can be made + break; + } + + } Until (-not $isContinuationToken) + + } + + # If all retry attempts have failed, throw an exception + $errorMessage = $script:localizedData.AzDevOpsApiRestMethodException -f $MyInvocation.MyCommand, $RetryAttempts, $restMethodExceptionMessage + throw (New-InvalidOperationException -Message $errorMessage) + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Flush-Log.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Flush-Log.ps1 new file mode 100644 index 000000000..3694487af --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Flush-Log.ps1 @@ -0,0 +1,38 @@ +<# +Function Flush-Log +{ + [CmdletBinding()] + param + () + + Write-Verbose "[Flush-Log] Started." + + # Write the log messages to the log files + Write-Verbose "[Flush-Log] Writing log messages to log files." + $LogFilePaths = @( + $Global:AZDO_LogSettings.VerboseLogFilePath + $Global:AZDO_LogSettings.WarningLogFilePath + $Global:AZDO_LogSettings.ErrorLogFilePath + ) + + $LogMessages = @( + $Global:AZDO_VerboseLog + $Global:AZDO_WarningLog + $Global:AZDO_ErrorLog + ) + + for ($i = 0; $i -lt $LogFilePaths.Count; $i++) + { + $LogFilePath = $LogFilePaths[$i] + $LogMessages = $LogMessages[$i] + + $LogMessages | Out-File -FilePath $LogFilePath -Append + + # Clear the log messages + $LogMessages.Clear() + $Global:AZDO_LogSettings."$($LogFilePath.Split('.')[-2])Count" = 0 + } + + Write-Verbose "[Flush-Log] Log messages written to log files." +} +#> diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Initialize-Log.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Initialize-Log.ps1 new file mode 100644 index 000000000..f8ba02bb6 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Initialize-Log.ps1 @@ -0,0 +1,34 @@ +<# +Function Initialize-Log { + [CmdletBinding()] + param ( + # The directory where the log files will be stored. + [Parameter()] + [string]$LogDirectory=$ENV:AZDODSC_CACHE_DIRECTORY + ) + + Write-Verbose "[Initialize-Log] Started." + + # Define the log path + $Global:AZDO_VerboseLog = [System.Collections.Generic.List[String]]::new() + $Global:AZDO_WarningLog = [System.Collections.Generic.List[String]]::new() + + $Global:AZDO_LogSettings = @{ + VerboseLogFilePath = Join-Path -Path $LogDirectory -ChildPath "Verbose.log" + WarningLogFilePath = Join-Path -Path $LogDirectory -ChildPath "Warning.log" + VerboseCount = 0 + WarningCount = 0 + LogCountLimit = 100 + } + + # Ensure the log directory exists + if (-not (Test-Path -Path $LogDirectory)) + { + Write-Verbose "[Initialize-Log] Log directory does not exist. Creating directory." + New-Item -ItemType Directory -Path $LogDirectory -Force | Out-Null + } + + # Initialize the log files + Write-Verbose "[Initialize-Log] Log files initialized at: $LogDirectory" +} +#> diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Error.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Error.ps1 new file mode 100644 index 000000000..cc95a6848 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Error.ps1 @@ -0,0 +1,51 @@ +<# +.SYNOPSIS +Logs an error message to a specified log file and displays the error message. + +.DESCRIPTION +The Write-Error function logs an error message to a specified log file and displays the error message using the original Write-Error cmdlet. +It appends the error message with a timestamp to the log file. + +.PARAMETER Message +The error message to be logged and displayed. This parameter is mandatory. + +.PARAMETER LogFilePath +The path to the log file where the error message will be appended. The default path is "C:\Temp\error_log.txt". + +.EXAMPLE +Write-Error -Message "An unexpected error occurred." + +This example logs the error message "An unexpected error occurred." to the default log file and displays the error message. + +.EXAMPLE +Write-Error -Message "An unexpected error occurred." -LogFilePath "C:\Logs\custom_error_log.txt" + +This example logs the error message "An unexpected error occurred." to the specified log file "C:\Logs\custom_error_log.txt" and displays the error message. + +.NOTES +The function uses the original Write-Error cmdlet from the Microsoft.PowerShell.Utility module to display the error message. +#> +Function Write-Error +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [Parameter()] + [string]$LogFilePath = "$($env:AZDO_ERRORLOGGING_FILEPATH)" + ) + + # Call the original Write-Error cmdlet to display the message + Microsoft.PowerShell.Utility\Write-Error $Message + $VerbosePreference = $originalPreference + + # Test if the env:enableVerboseLogging variable is set to true + if (-not [String]::IsNullOrEmpty($LogFilePath)) + { + # Append the message to the log file + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Add-Content -Path $LogFilePath -Value "[$timestamp] $Message" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Verbose.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Verbose.ps1 new file mode 100644 index 000000000..358b4f743 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Verbose.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS +Writes a verbose message to the console and optionally logs it to a file. + +.DESCRIPTION +The Write-Verbose function writes a verbose message to the console using the built-in Write-Verbose cmdlet. +specified by the LogFilePath parameter. + +.PARAMETER Message +The verbose message to be written to the console and optionally logged to a file. + +.PARAMETER LogFilePath +The path to the log file where the verbose message will be appended. If not specified, the value of the + +.EXAMPLE +Write-Verbose -Message "This is a verbose message." + +This command writes the message "This is a verbose message." to the console and, if the environment variable + +.NOTES +The function uses the built-in Write-Verbose cmdlet to write messages to the console. It also checks if the +#> +Function Write-Verbose +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [Parameter()] + [string]$LogFilePath = "$($env:AZDO_VERBOSELOGGING_FILEPATH)" + ) + + # Call the original Write-Verbose cmdlet to display the message if verbose preference is enabled + Microsoft.PowerShell.Utility\Write-Verbose $Message + + # Test if the env:enableVerboseLogging variable is set to true + if (-not [String]::IsNullOrEmpty($LogFilePath)) + { + # Append the message to the log file + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Add-Content -Path $LogFilePath -Value "[$timestamp] $Message" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Warning.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Warning.ps1 new file mode 100644 index 000000000..ff5a80dc0 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Proxy Functions/Write-Warning.ps1 @@ -0,0 +1,51 @@ +<# +.SYNOPSIS +Writes a warning message to the console and appends it to a log file. + +.DESCRIPTION +The Write-Warning function writes a warning message to the console using the original Write-Verbose cmdlet if the verbose preference is enabled. It also appends the warning message to a specified log file with a timestamp. + +.PARAMETER Message +The warning message to be written to the console and log file. This parameter is mandatory. + +.PARAMETER LogFilePath +The path to the log file where the warning message will be appended. The default path is "C:\Temp\warning_log.txt". + +.EXAMPLE +Write-Warning -Message "This is a warning message." + +This example writes the warning message "This is a warning message." to the console and appends it to the default log file. + +.EXAMPLE +Write-Warning -Message "This is a warning message." -LogFilePath "C:\Logs\custom_warning_log.txt" + +This example writes the warning message "This is a warning message." to the console and appends it to the specified log file "C:\Logs\custom_warning_log.txt". + +.NOTES +The function temporarily sets the $VerbosePreference to 'Continue' to ensure the warning message is displayed if verbose preference is enabled, and then restores the original preference. +#> +Function Write-Warning +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [Parameter()] + [string]$LogFilePath = "$($env:AZDO_WARNINGLOGGING_FILEPATH)" + ) + + # Call the original Write-Verbose cmdlet to display the message if verbose preference is enabled + $originalPreference = $VerbosePreference + $VerbosePreference = 'Continue' + Microsoft.PowerShell.Utility\Write-Warning $Message + $VerbosePreference = $originalPreference + + if (-not [String]::IsNullOrEmpty($LogFilePath)) + { + # Append the message to the log file + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Add-Content -Path $LogFilePath -Value "[$timestamp] $Message" + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Write-Log.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Write-Log.ps1 new file mode 100644 index 000000000..0aa72b804 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Logging/Write-Log.ps1 @@ -0,0 +1,53 @@ +<# +Function Write-Log +{ + [CmdletBinding()] + param + ( + # The message to be logged. + [Parameter(Mandatory = $true)] + [string]$Message, + + # The type of log message. + [Parameter(Mandatory = $true)] + [ValidateSet('Verbose', 'Warning')] + [string]$Type + ) + + Write-Verbose "[Write-Log] Started." + + # Get the current date and time + $DateTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + + # Construct the log message + $LogMessage = "[$DateTime] $Message" + + # Write the log message to the appropriate log file + switch ($Type) + { + 'Verbose' { $Global:AZDO_VerboseLog.Add($LogMessage) } + 'Warning' { $Global:AZDO_WarningLog.Add($LogMessage) } + 'Error' { $Global:AZDO_ErrorLog.Add($LogMessage) } + } + + # Increment the log count + $Global:AZDO_LogSettings."$Type"Count++ + + # Check if the log count limit has been reached + if ($Global:AZDO_LogSettings."$Type"Count -ge $Global:AZDO_LogSettings.LogCountLimit) + { + # Write the log messages to the log file + Write-Verbose "[Write-Log] Writing log messages to log file." + $LogFilePath = $Global:AZDO_LogSettings."$Type"LogFilePath + $LogMessages = $Global:AZDO_"$Type"Log + $LogMessages | Out-File -FilePath $LogFilePath -Append + + # Clear the log messages + $Global:AZDO_"$Type"Log = [System.Collections.Generic.List[String]]::new() + $Global:AZDO_LogSettings."$Type"Count = 0 + } + + Write-Verbose "[Write-Log] Log message written to log file." + +} +#> diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-AzDevOpsACLToken.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-AzDevOpsACLToken.ps1 new file mode 100644 index 000000000..0c77d46de --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-AzDevOpsACLToken.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS +Creates a token for Azure DevOps access control. + +.DESCRIPTION +The New-AzDevOpsACLToken function creates a token for Azure DevOps access control. +The token can be used to grant access at either the project level or the team level. + +.PARAMETER OrganizationName +The name of the Azure DevOps organization. + +.PARAMETER ProjectId +The ID of the Azure DevOps project. + +.PARAMETER TeamId +The ID of the Azure DevOps team. If not specified, the token will be created for project-level access. + +.EXAMPLE +New-AzDevOpsACLToken -OrganizationName "Contoso" -ProjectId "MyProject" -TeamId "MyTeam" +Creates a token for team-level access to the specified Azure DevOps project and team. + +.EXAMPLE +New-AzDevOpsACLToken -OrganizationName "Contoso" -ProjectId "MyProject" +Creates a token for project-level access to the specified Azure DevOps project. +#> + +function New-AzDevOpsACLToken +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter(Mandatory = $true)] + [string]$ProjectId, + + [Parameter()] + [string]$TeamId + ) + + process + { + if ($TeamId) + { + # Construct a token for team-level access + $token = "vstfs:///Classification/TeamProject/$ProjectId/$TeamId" + } + else + { + # Construct a token for project-level access + $token = "vstfs:///Classification/TeamProject/$ProjectId" + } + + return $token + + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-InvalidOperationException.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-InvalidOperationException.ps1 new file mode 100644 index 000000000..5aba84d0e --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-InvalidOperationException.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS +Creates a new InvalidOperationException error record. + +.DESCRIPTION +The New-InvalidOperationException function generates a new ErrorRecord object for an InvalidOperationException with a specified message. Optionally, it can throw the error. + +.PARAMETER Message +The message that describes the error. This parameter is mandatory and cannot be null or empty. + +.PARAMETER Throw +A switch parameter that, if specified, will throw the generated ErrorRecord instead of returning it. + +.OUTPUTS +System.Management.Automation.ErrorRecord + +.EXAMPLE +PS> New-InvalidOperationException -Message "An invalid operation occurred." + +Creates and returns an ErrorRecord for an InvalidOperationException with the specified message. + +.EXAMPLE +PS> New-InvalidOperationException -Message "An invalid operation occurred." -Throw + +Creates and throws an ErrorRecord for an InvalidOperationException with the specified message. +#> +using namespace System.Management.Automation + +function New-InvalidOperationException +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.ErrorRecord])] + param ( + [Parameter( + Position = 0, + Mandatory + )] + [ValidateNotNullOrEmpty()] + [string] + $Message, + + [Parameter()] + [switch] + $Throw + ) + process + { + $ErrorRecord = [ErrorRecord]::new( + [InvalidOperationException]::new($Message), + "System.InvalidOperationException", + [ErrorCategory]::ConnectionError, + $null + ) + + if ($Throw) + { + throw $ErrorRecord + } + + Write-Output $ErrorRecord + + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Invoke-AzDevOpsApiRestMethod.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Invoke-AzDevOpsApiRestMethod.ps1 deleted file mode 100644 index d2b147534..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Invoke-AzDevOpsApiRestMethod.ps1 +++ /dev/null @@ -1,131 +0,0 @@ -<# - .SYNOPSIS - This is a light, generic, wrapper proceedure around 'Invoke-RestMethod' to handle - multiple retries and error/exception handling. - - This function makes no assumptions around the versions of the API used, the resource - being operated/actioned upon, the operation/method being performed, nor the content - of the HTTP headers and body. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER HttpMethod - The HTTP method being used in the HTTP/REST request sent to the Azure DevOps API. - - .PARAMETER HttpHeaders - The headers for the HTTP/REST request sent to the Azure DevOps API. - - .PARAMETER HttpBody - The body for the HTTP/REST request sent to the Azure DevOps API. If performing a 'Post', - 'Put' or 'Patch' method/request, this will typically contain the JSON document of the resource. - - .PARAMETER RetryAttempts - The number of times the method/request will attempt to be resent/retried if unsuccessful on the - initial attempt. - - If any attempt is successful, the remaining attempts are ignored. - - .PARAMETER RetryIntervalMs - The interval (in Milliseconds) between retry attempts. - - .EXAMPLE - Invoke-AzDevOpsApiRestMethod -ApiUri 'YourApiUriHere' -HttpMethod 'Get' -HttpHeaders $YouHttpHeadersHashtableHere - - Submits a 'Get' request to the Azure DevOps API (relying on the 'ApiUri' value to determine what is being retrieved). - - .EXAMPLE - Invoke-AzDevOpsApiRestMethod -ApiUri 'YourApiUriHere' -HttpMethod 'Patch' -HttpHeaders $YourHttpHeadersHashtableHere ` - -HttpBody $YourHttpBodyHere -RetryAttempts 3 - - Submits a 'Patch' request to the Azure DevOps API with the supplied 'HttpBody' and will attempt to retry 3 times (4 in - total, including the intitial attempt) if unsuccessful. -#> -function Invoke-AzDevOpsApiRestMethod -{ - [CmdletBinding()] - [OutputType([System.Management.Automation.PSObject])] - param - ( - [Parameter(Mandatory=$true)] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter(Mandatory=$true)] - [ValidateSet('Get','Post','Patch','Put','Delete')] - [System.String] - [Alias('Method')] - $HttpMethod, - - [Parameter(Mandatory=$true)] - [ValidateScript( { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $_ -IsValid })] - [Hashtable] - [Alias('Headers','HttpRequestHeader')] - $HttpHeaders, - - [Parameter()] - [System.String] - [Alias('Body')] - $HttpBody, - - [Parameter()] - [System.String] - [Alias('ContentType')] - [ValidateSet('application/json')] - $HttpContentType = 'application/json', - - [Parameter()] - [ValidateRange(0,5)] - [Int32] - $RetryAttempts = 5, - - [Parameter()] - [ValidateRange(250,10000)] - [Int32] - $RetryIntervalMs = 250 - ) - - $invokeRestMethodParameters = @{ - Uri = $ApiUri - Method = $HttpMethod - Headers = $HttpHeaders - Body = $HttpBody - ContentType = $HttpContentType - } - - # Remove the 'Body' and 'ContentType' if not relevant to request - if ($HttpMethod -in $('Get','Delete')) - { - $invokeRestMethodParameters.Remove('Body') - $invokeRestMethodParameters.Remove('ContentType') - } - - # Intially set this value to -1, as the first attempt does not want to be classed as a "RetryAttempt" - $CurrentNoOfRetryAttempts = -1 - - while ($CurrentNoOfRetryAttempts -lt $RetryAttempts) - { - try - { - return Invoke-RestMethod @invokeRestMethodParameters - } - catch - { - # Increment the number of retries attempted and obtain any exception message - $CurrentNoOfRetryAttempts++ - $restMethodExceptionMessage = $_.Exception.Message - - # Wait before the next attempt/retry - Start-Sleep -Milliseconds $RetryIntervalMs - } - } - - - # If all retry attempts have failed, throw an exception - $errorMessage = $script:localizedData.AzDevOpsApiRestMethodException -f $MyInvocation.MyCommand, $RetryAttempts, $restMethodExceptionMessage - New-InvalidOperationException -Message $errorMessage - -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/New-AzDevOpsApiResource.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/New-AzDevOpsApiResource.ps1 deleted file mode 100644 index 6a1a36308..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/New-AzDevOpsApiResource.ps1 +++ /dev/null @@ -1,115 +0,0 @@ -<# - .SYNOPSIS - Attempts to create an resource within Azure DevOps. - - The type of resource type created is provided in the 'ResourceName' parameter and it is - assumed that the 'Resource' parameter value passed in meets the specification of the resource. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER ApiVersion - The version of the Azure DevOps API to use in the call/execution to/against the API. - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ResourceName - The name of the resource being created within Azure DevOps (e.g. 'Project') - - .PARAMETER Resource - The resource being created (typically provided by another function (e.g. 'New-AzDevOpsApiProject')). - - .EXAMPLE - New-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' -Resource $YourResource -Wait - - Creates the 'Project' resource in Azure DevOps within to the Organization relating to the to the 'ApiUri' - provided. - - NOTE: In this example, the '-Wait' switch is provided so the function will wait for the corresponding API 'Operation' - to complete before the function completes. No return value is provided by this function and if the creation of the - resource has failed, an exception will be thrown. - - .EXAMPLE - New-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' -Resource $YourResource - - Creates the 'Project' resource in Azure DevOps within to the Organization relating to the to the 'ApiUri' - provided. - - NOTE: In this example, no '-Wait' switch is provided so the request is made to the API but the operation may - not complete before the function completes (and may not complete successfully at all). -#> -function New-AzDevOpsApiResource -{ - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter()] - [ValidateScript( { Test-AzDevOpsApiVersion -ApiVersion $_ -IsValid })] - [System.String] - $ApiVersion = $(Get-AzDevOpsApiVersion -Default), - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsApiResourceName -ResourceName $_ -IsValid })] - [System.String] - $ResourceName, - - [Parameter(Mandatory = $true)] - [System.Object] - $Resource, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Wait, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Force - ) - - if ($Force -or $PSCmdlet.ShouldProcess($apiResourceUri, $ResourceName)) - { - $apiResourceUriParameters = @{ - ApiUri = $ApiUri - ApiVersion = $ApiVersion - ResourceName = $ResourceName - } - - [string]$apiResourceUri = Get-AzDevOpsApiResourceUri @apiResourceUriParameters - [Hashtable]$apiHttpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - [string]$apiHttpRequestBody = $Resource | ConvertTo-Json -Depth 10 -Compress - - [System.Object]$apiOperation = $null - [System.Object]$apiOperation = Invoke-AzDevOpsApiRestMethod -Uri $apiResourceUri -Method 'Post' ` - -Headers $apiHttpRequestHeader -Body $apiHttpRequestBody ` - -ContentType 'application/json' - - if ($Wait) - { - # Waits for operation to complete successfully. Throws exception if operation is not successful and/or timeout is reached. - Wait-AzDevOpsOperation -ApiUri $ApiUri -Pat $Pat ` - -OperationId $apiOperation.id ` - -IsSuccessful - - # Adds an additional, post-operation delay/buffer to mitigate subsequent calls trying to obtain new/updated items too quickly from the API - Start-Sleep -Milliseconds $(Get-AzDevOpsApiWaitIntervalMs) - } - } -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Remove-AzDevOpsApiResource.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Remove-AzDevOpsApiResource.ps1 deleted file mode 100644 index b503e19ca..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Remove-AzDevOpsApiResource.ps1 +++ /dev/null @@ -1,122 +0,0 @@ -<# - .SYNOPSIS - Attempts to remove a resource within Azure DevOps. - - The type of resource type removed is provided in the 'ResourceName' parameter and it is - assumed that the 'ResourceId' parameter value passed in is present (in order to be deleted). - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER ApiVersion - The version of the Azure DevOps API to use. Defaults to value suppored by the module. - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ResourceName - The name of the resource being deleted within Azure DevOps (e.g. 'Project') - - .PARAMETER ResourceId - The 'ResourceId' of the resource being created (typically provided by another function (e.g. 'Remove-AzDevOpsApiProject')). - - .PARAMETER Wait - Using this switch ensures that the execution will run synchronously and wait for the resource to be removed before - continuing. By not using this switch, execution will run asynchronously. - - .PARAMETER Force - Using this switch will override any confirmations prior to the deletion/removal of the resource. - - .EXAMPLE - Remove-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' -ResourceId $YourResourceId -Wait - - Removes/Deletes the 'Project' resource in Azure DevOps within to the Organization relating to the to the 'ApiUri' - provided. - - NOTE: In this example, the '-Wait' switch is provided so the function will wait for the corresponding API 'Operation' - to complete before the function completes. No return value is provided by this function and if the creation of the - resource has failed, an exception will be thrown. - - .EXAMPLE - New-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' -ResourceId $YourResourceId - - Remmoves/Deletes the 'Project' resource in Azure DevOps within to the Organization relating to the to the 'ApiUri' - provided. - - NOTE: In this example, no '-Wait' switch is provided so the request is made to the API but the operation may - not complete before the function completes (and may not complete successfully at all). -#> -function Remove-AzDevOpsApiResource -{ - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter()] - [ValidateScript( { Test-AzDevOpsApiVersion -ApiVersion $_ -IsValid })] - [System.String] - $ApiVersion = $(Get-AzDevOpsApiVersion -Default), - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsApiResourceName -ResourceName $_ -IsValid })] - [System.String] - $ResourceName, - - [Parameter(Mandatory = $true)] - [System.Object] - [ValidateScript({ Test-AzDevOpsApiResourceId -ResourceId $_ -IsValid })] - $ResourceId, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Wait, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Force - ) - - if ($Force -or $PSCmdlet.ShouldProcess($apiResourceUri, $ResourceName)) - { - $apiResourceUriParameters = @{ - ApiUri = $ApiUri - ApiVersion = $ApiVersion - ResourceName = $ResourceName - ResourceId = $ResourceId - } - - [string]$apiResourceUri = Get-AzDevOpsApiResourceUri @apiResourceUriParameters - [Hashtable]$apiHttpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - - [System.Object]$apiOperation = $null - [System.Object]$apiOperation = Invoke-AzDevOpsApiRestMethod -Uri $apiResourceUri -Method 'Delete' ` - -Headers $apiHttpRequestHeader - - if ($Wait) - { - # Waits for operation to complete successfully. Throws exception if operation is not successful and/or timeout is reached. - Wait-AzDevOpsOperation -ApiUri $ApiUri -Pat $Pat ` - -OperationId $apiOperation.id ` - -IsSuccessful - - # Adds an additional, post-operation delay/buffer to mitigate subsequent calls trying to obtain new/updated items too quickly from the API - Start-Sleep -Milliseconds $(Get-AzDevOpsApiWaitIntervalMs) - } - } -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Set-AzDevOpsApiResource.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Set-AzDevOpsApiResource.ps1 deleted file mode 100644 index 0f748c403..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Set-AzDevOpsApiResource.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -<# - .SYNOPSIS - Attempts to update a resource within Azure DevOps. - - The type of resource type updated is provided in the 'ResourceName' parameter and it is - assumed that the 'Resource' parameter value passed in meets the specification of the resource. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER ApiVersion - The version of the Azure DevOps API to use in the call/execution to/against the API. - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ResourceName - The name of the resource being updated within Azure DevOps (e.g. 'Project') - - .PARAMETER Resource - The resource being updated (typically provided by another function (e.g. 'Set-AzDevOpsApiProject')). - - .EXAMPLE - Set-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' -Resource $YourResource -Wait - - Updates the 'Project' resource in Azure DevOps within to the Organization relating to the to the 'ApiUri' - provided. - - NOTE: In this example, the '-Wait' switch is provided so the function will wait for the corresponding API 'Operation' - to complete before the function completes. No return value is provided by this function and if the creation of the - resource has failed, an exception will be thrown. - - .EXAMPLE - Set-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ResourceName 'Project' -Resource $YourResource - - Updates the 'Project' resource in Azure DevOps within to the Organization relating to the to the 'ApiUri' - provided. - - NOTE: In this example, no '-Wait' switch is provided so the request is made to the API but the operation may - not complete before the function completes (and may not complete successfully at all). -#> -function Set-AzDevOpsApiResource -{ - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter()] - [ValidateScript( { Test-AzDevOpsApiVersion -ApiVersion $_ -IsValid })] - [System.String] - $ApiVersion = $(Get-AzDevOpsApiVersion -Default), - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsApiResourceName -ResourceName $_ -IsValid })] - [System.String] - $ResourceName, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsApiResourceId -ResourceId $_ -IsValid })] - [System.String] - $ResourceId, - - [Parameter(Mandatory = $true)] - [System.Object] - $Resource, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Wait, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Force - ) - - if ($Force -or $PSCmdlet.ShouldProcess($apiResourceUri, $ResourceName)) - { - $apiResourceUriParameters = @{ - ApiUri = $ApiUri - ApiVersion = $ApiVersion - ResourceName = $ResourceName - ResourceId = $ResourceId - } - - [string]$apiResourceUri = Get-AzDevOpsApiResourceUri @apiResourceUriParameters - [Hashtable]$apiHttpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - [string]$apiHttpRequestBody = $Resource | ConvertTo-Json -Depth 10 -Compress - - [System.Object]$apiOperation = $null - [System.Object]$apiOperation = Invoke-AzDevOpsApiRestMethod -Uri $apiResourceUri -Method 'Patch' ` - -Headers $apiHttpRequestHeader -Body $apiHttpRequestBody ` - -ContentType 'application/json' - - if ($Wait) - { - # Waits for operation to complete successfully. Throws exception if operation is not successful and/or timeout is reached. - Wait-AzDevOpsOperation -ApiUri $ApiUri -Pat $Pat ` - -OperationId $apiOperation.id ` - -IsSuccessful - - # Adds an additional, post-operation delay/buffer to mitigate subsequent calls trying to obtain new/updated items too quickly from the API - Start-Sleep -Milliseconds $(Get-AzDevOpsApiWaitIntervalMs) - } - } -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResource.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResource.ps1 deleted file mode 100644 index fcdb07a69..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResource.ps1 +++ /dev/null @@ -1,73 +0,0 @@ -<# - .SYNOPSIS - Tests for the presence of an Azure DevOps API Resource. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER ApiVersion - The version of the Azure DevOps API to use in the call/execution to/against the API. - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/Resources - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent Resources being performed. - - .PARAMETER ResourceName - The name of the resource being updated within Azure DevOps (e.g. 'Project') - - .PARAMETER ResourceId - The 'id' of the Azure DevOps API Resource. This is typically obtained from a response - provided by the API when a request is made to it. - - .EXAMPLE - Test-AzDevOpsApiResource -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' ` - -ResourceName 'YourResourceName' -ResourceId 'YourResourceId' - - Tests that the Azure DevOps 'Resource' (identified by the 'ResourceId' for the resource of type - provided by the 'ResourceName' field) exists. Returns $true if it exists and returns $false - if it does not. -#> -function Test-AzDevOpsApiResource -{ - [CmdletBinding()] - [OutputType([System.Boolean])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter()] - [ValidateScript( { Test-AzDevOpsApiVersion -ApiVersion $_ -IsValid })] - [System.String] - $ApiVersion = $(Get-AzDevOpsApiVersion -Default), - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsApiResourceName -ResourceName $_ -IsValid })] - [System.String] - $ResourceName, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsApiResourceId -ResourceId $_ -IsValid })] - [Alias('Id')] - [System.String] - $ResourceId - ) - - [System.Management.Automation.PSObject[]]$apiResource = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat ` - -ResourceName $ResourceName ` - -ResourceId $ResourceId - - return ($apiResource.Count -gt 0) -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceName.ps1 b/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceName.ps1 deleted file mode 100644 index 1fd11df8f..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceName.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -<# - .SYNOPSIS - Peforms test on a provided 'ResourceName' to provide a boolean ($true or $false) - return value. Returns $true if the test is successful. - - NOTE: Use of the '-IsValid' switch is required. - - .PARAMETER ResourceName - The 'ResourceName' to be tested/validated. - - .PARAMETER IsValid - Use of this switch will validate the format of the 'ResourceName' - rather than the existence/presence of it. - - Failure to use this switch will throw an exception. - - .EXAMPLE - Test-AzDevOpsApiResourceName -ResourceName 'YourResourceNameHere' -IsValid - - Returns $true if the 'ResourceName' provided is of a valid format. - Returns $false if it is not. -#> -function Test-AzDevOpsApiResourceName -{ - [CmdletBinding()] - [OutputType([System.Boolean])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $ResourceName, - - [Parameter(Mandatory = $true)] - [ValidateSet($true)] - [System.Management.Automation.SwitchParameter] - $IsValid - ) - - - return !(!($(Get-AzDevOpsApiResourceName).Contains($ResourceName))) -} diff --git a/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psd1 b/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psd1 index f10821fd2..cf8dfccdd 100644 --- a/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psd1 +++ b/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psd1 @@ -22,17 +22,67 @@ # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( + # + # LCM Supporting Functions + 'Get-AzDevOpsServicesUri', 'Get-AzDevOpsServicesApiUri', - 'Get-AzDevOpsOperation', 'Test-AzDevOpsOperation', + 'Initialize-CacheObject', + + 'New-AzDoAuthenticationProvider', + 'Get-AzDoCacheObjects', + 'AzDoAPI_0_ProjectCache', + 'AzDoAPI_1_GroupCache', + 'AzDoAPI_2_UserCache', + 'AzDoAPI_3_GroupMemberCache', + + # + # DSC Class Based Resources + 'Get-AzDoProject', + 'New-AzDoProject', + 'Set-AzDoProject', + 'Remove-AzDoProject', + 'Test-AzDoProject', + + 'Get-AzDoProjectGroup', + 'New-AzDoProjectGroup', + 'Set-AzDoProjectGroup', + 'Remove-AzDoProjectGroup', + 'Test-AzDoProjectGroup', + + 'Get-AzDoOrganizationGroup', + 'New-AzDoOrganizationGroup', + 'Set-AzDoOrganizationGroup', + 'Remove-AzDoOrganizationGroup', + 'Test-AzDoOrganizationGroup', + + 'Get-AzDoGroupMember', + 'New-AzDoGroupMember', + 'Set-AzDoGroupMember', + 'Remove-AzDoGroupMember', + 'Test-AzDoGroupMember' + + 'Get-AzDoGitRepository', + 'New-AzDoGitRepository', + 'Remove-AzDoGitRepository', + + 'Get-AzDoGitPermission', + 'New-AzDoGitPermission', + 'Remove-AzDoGitPermission', + 'Set-AzDoGitPermission', + + 'Get-AzDoGroupPermission', + 'New-AzDoGroupPermission', + 'Remove-AzDoGroupPermission', + 'Set-AzDoGroupPermission', + + 'Get-AzDoProjectServices', + 'Set-AzDoProjectServices', + 'Test-AzDoProjectServices', + 'Remove-AzDoProjectServices' - 'Get-AzDevOpsProject', - 'New-AzDevOpsProject', - 'Set-AzDevOpsProject', - 'Remove-AzDevOpsProject', - 'Test-AzDevOpsProject' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. diff --git a/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psm1 b/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psm1 index f47986a47..380a2fc75 100644 --- a/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psm1 +++ b/source/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.psm1 @@ -1,28 +1,49 @@ +[CmdletBinding()] +param ( + [Parameter()] + [Switch] + $isClass +) + # Setup/Import 'DscResource.Common' helper module #$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' #Import-Module -Name $script:resourceHelperModulePath - $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +$ModuleRoot = $PSScriptRoot # Obtain all functions within PSModule $functionSubDirectoryPaths = @( + # Classes + "$ModuleRoot\Api\Classes\", + + # Enum + #"$ModuleRoot\Api\Enums\", + + # Data + #"$ModuleRoot\Api\Data\", + "$ModuleRoot\LocalizedData\", + # Api - "$PSScriptRoot\Api\Functions\Private", + "$ModuleRoot\Api\Functions\Private\Api", + "$ModuleRoot\Api\Functions\Private\Cache", + "$ModuleRoot\Api\Functions\Private\Helper", + "$ModuleRoot\Api\Functions\Private\Cache\Cache Initalization" + "$ModuleRoot\Api\Functions\Private\Authentication" # Connection - "$PSScriptRoot\Connection\Functions\Private", + "$ModuleRoot\Connection\Functions\Private", # Resources - "$PSScriptRoot\Resources\Functions\Public", - "$PSScriptRoot\Resources\Functions\Private", + "$ModuleRoot\Resources\Functions\Public", + "$ModuleRoot\Resources\Functions\Private", # Server # Services - "$PSScriptRoot\Services\Functions\Public" + "$ModuleRoot\Services\Functions\Public" ) $functions = Get-ChildItem -Path $functionSubDirectoryPaths -Recurse -Include "*.ps1" @@ -37,9 +58,28 @@ foreach ($function in $functions) ) ) - if ($function.FullName -ilike "$PSScriptRoot\*\Functions\Public\*") + if ($function.FullName -ilike "$ModuleRoot\*\Functions\Public\*") { Write-Verbose "Exporting '$($function.BaseName)'..." Export-ModuleMember -Function $($function.BaseName) } } + +# +# Static Functions that need to be exported + +Export-ModuleMember -Function 'AzDoAPI_*' + +Export-ModuleMember -Function 'Set-CacheObject' +Export-ModuleMember -Function 'Get-CacheItem' +Export-ModuleMember -Function 'Get-AzDoAPIGroupCache' +Export-ModuleMember -Function 'Get-AzDoAPIProjectCache' +Export-ModuleMember -Function 'Initialize-CacheObject' +Export-ModuleMember -Function 'Get-AzDoCacheObjects' +Export-ModuleMember -Function '*-AzDoProjectGroup' + +# Stop processing +if ($isClass) +{ + return +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Connection/Functions/Private/Test-AzDevOpsPatCredential.ps1 b/source/Modules/AzureDevOpsDsc.Common/Connection/Functions/Private/Test-AzDevOpsPatCredential.ps1 index 293087ae8..d5a111482 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Connection/Functions/Private/Test-AzDevOpsPatCredential.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Connection/Functions/Private/Test-AzDevOpsPatCredential.ps1 @@ -37,6 +37,6 @@ function Test-AzDevOpsPatCredential $IsValid ) - return !($null -eq $PatCredential -or - 'PAT' -ne $PatCredential.UserName) + return (-not($null -eq $PatCredential -or + 'PAT' -ne $PatCredential.UserName)) } diff --git a/source/Modules/AzureDevOpsDsc.Common/LocalizedData/000.LocalizedDataAzACLTokenPatten.ps1 b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/000.LocalizedDataAzACLTokenPatten.ps1 new file mode 100644 index 000000000..69504c45c --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/000.LocalizedDataAzACLTokenPatten.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Contains localized data for Azure DevOps ACL token patterns. + +.DESCRIPTION + This data section defines various regular expression patterns used for matching Azure DevOps ACL tokens. + These patterns are used to identify and extract information from different components such as organizations, + projects, repositories, branches, groups, and resources within Azure DevOps. + +.KEYWORDS + Azure DevOps, ACL, Token Patterns, Regular Expressions + +.EXAMPLES + The patterns can be used to match and extract information from ACL tokens in Azure DevOps: + + - OrganizationGit: Matches the organization token. + - GitProject: Matches the project token and extracts the ProjectId. + - GitRepository: Matches the repository token and extracts the ProjectId and RepoId. + - GitBranch: Matches the branch token and extracts the ProjectId, RepoId, and BranchName. + - GroupPermission: Matches the group permission token and extracts the ProjectId and GroupId. + - ResourcePermission: Matches the resource permission token and extracts the ProjectId. +#> +data LocalizedDataAzACLTokenPatten +{ + @{ + # Git ACL Token Patterns + OrganizationGit = '^repoV2$' + GitProject = '^(repoV2)\/(?[A-Za-z0-9-]+)$' + GitRepository = '^(repoV2)\/(?[A-Za-z0-9-]+)\/(?[A-Za-z0-9-]+)$' + GitBranch = '^(repoV2)\/(?[A-Za-z0-9-]+)\/(?[A-Za-z0-9-]+)\/refs\/heads\/(?[A-Za-z0-9]+)' + # Identity ACL Token Patterns + GroupPermission = '^(?[A-Za-z0-9-_]+)\\(?[A-Za-z0-9-_]+)$' + ResourcePermission = '^(?[A-Za-z0-9-_]+)$' + } +} diff --git a/source/Modules/AzureDevOpsDsc.Common/LocalizedData/001.LocalizedDataAzResourceTokenPatten.ps1 b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/001.LocalizedDataAzResourceTokenPatten.ps1 new file mode 100644 index 000000000..8c3788687 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/001.LocalizedDataAzResourceTokenPatten.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Contains localized data for Azure DevOps resource token patterns. + +.DESCRIPTION + This data section defines various regular expression patterns used for matching Azure DevOps resource tokens. + These patterns are used to identify and extract information from different Azure DevOps resources such as organizations, projects, repositories, and permissions. + +.KEYWORDS + Azure DevOps, Regular Expressions, Token Patterns, Localization + +.NOTES + Filepath: /c:/Git/AzureDevOpsDsc/source/Modules/AzureDevOpsDsc.Common/LocalizedData/001.LocalizedDataAzResourceTokenPatten.ps1 + +.EXAMPLES + # Example usage of the data section + $localizedData = LocalizedDataAzResourceTokenPatten + $orgPattern = $localizedData.OrganizationGit + $projectPattern = $localizedData.GitProject + $repoPattern = $localizedData.GitRepository + $groupPermissionPattern = $localizedData.GroupPermission + $resourcePermissionPattern = $localizedData.ResourcePermission + $projectPermissionPattern = $localizedData.ProjectPermission +#> + +data LocalizedDataAzResourceTokenPatten +{ + @{ + # Git ACL Token Patterns + OrganizationGit = '^azdoorg$' + GitProject = '^\(repoV2\)\\/\(\?[A-Za-z0-9-]+\)$' + GitRepository = '(?[A-Za-z0-9-_]+)(\/|\\)(?[A-Za-z0-9-_]+)' + # Identity ACL Token Patterns + GroupPermission = '^(?[A-Za-z0-9-_]+)\\(?[A-Za-z0-9-_]+)$' + ResourcePermission = '^\(\?[A-Za-z0-9-_]+\)$' + ProjectPermission = '^\$PROJECT:vstfs:\/{3}Classification\/TeamProject\/(?[A-Za-z0-9-_]+)$' + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/LocalizedData/002.LocalizedDataAzSerializationPatten.ps1 b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/002.LocalizedDataAzSerializationPatten.ps1 new file mode 100644 index 000000000..295445377 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/002.LocalizedDataAzSerializationPatten.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Contains localized data patterns for Azure DevOps serialization. + +.DESCRIPTION + This data section defines regular expression patterns used for matching various Azure DevOps entities such as Git repositories, group permissions, and project permissions. + +.NOTES + +.DATA + GitRepository + Pattern to match Git repository ACL tokens, excluding branch level ACLs. + Example: repoV2/ProjectId/RepoId + Excludes: repoV2/ProjectId/RepoId/refs/heads/BranchName + + GroupPermission + Pattern to match group permissions. + Example: 78a5065f-3043-426f-9cc5-785748b18f9d\\242ea4ca-e150-4499-a491-00f4ce1f480e + + ProjectPermission + Pattern to match project permissions. + Example: $PROJECT:vstfs:///Classification/TeamProject/78a5065f-3043-426f-9cc5-785748b18f9d +#> +data LocalizedDataAzSerializationPatten +{ + @{ + # Git Repository ACL Token Patterns. Exclude the refs token since these are branch level ACLs. + # Example: repoV2/ProjectId/RepoId + # Not: repoV2/ProjectId/RepoId/refs/heads/BranchName + GitRepository = '^repoV2\/{0}\/(?!.*\/refs).*' + # Group Permissions + # Example: 78a5065f-3043-426f-9cc5-785748b18f9d\\242ea4ca-e150-4499-a491-00f4ce1f480e + GroupPermission = '^{0}\\\\{1}$' + # Project Permissions + # Example: $PROJECT:vstfs:///Classification/TeamProject/78a5065f-3043-426f-9cc5-785748b18f9d + ProjectPermission = '^\$PROJECT:vstfs:\/{3}Classification\/TeamProject\/{0}$' + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/LocalizedData/003.LocalizedDataAzURLParams.ps1 b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/003.LocalizedDataAzURLParams.ps1 new file mode 100644 index 000000000..415034a1c --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/LocalizedData/003.LocalizedDataAzURLParams.ps1 @@ -0,0 +1,13 @@ +data LocalizedDataAzURLParams +{ +@' +# +# Azure DevOps Project Services URL Parameters + +ProjectService_Pipelines=ms.vss-build.pipelines +ProjectService_TestPlans=ms.vss-test-web.test +ProjectService_Boards=ms.vss-work.agile +ProjectService_Repos=ms.vss-code.version-control +ProjectService_Artifacts=ms.azure-artifacts.feature +'@ | ConvertFrom-StringData +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.ps1 index fb8aa0e17..ebb86965c 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.ps1 @@ -36,10 +36,13 @@ function Test-AzDevOpsOrganizationName $IsValid ) - return !([System.String]::IsNullOrWhiteSpace($OrganizationName) -or - ($OrganizationName.Contains(' ') -or - $OrganizationName.Contains('%') -or - $OrganizationName.Contains('*') -or - $OrganizationName.StartsWith(' ') -or - $OrganizationName.EndsWith(' '))) + return (-not([System.String]::IsNullOrWhiteSpace($OrganizationName)) -or + ( + $OrganizationName.Contains(' ') -or + $OrganizationName.Contains('%') -or + $OrganizationName.Contains('*') -or + $OrganizationName.StartsWith(' ') -or + $OrganizationName.EndsWith(' ') + ) + ) } diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.ps1 index f58cab88d..74f6d6be8 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.ps1 @@ -26,16 +26,22 @@ function Test-AzDevOpsPat [OutputType([System.Boolean])] param ( - [Parameter(Mandatory = $true)] + [Parameter()] [System.String] $Pat, - [Parameter(Mandatory = $true)] - [ValidateSet($true)] + [Parameter()] [System.Management.Automation.SwitchParameter] $IsValid ) - return !([System.String]::IsNullOrWhiteSpace($Pat) -or - $Pat.Length -ne 52) # Note: 52 is the current/expected length of PAT + # If the Pat token is blank it means that managed identity is being used. + # In this case, the function will return $true. + + if ([System.String]::IsNullOrWhiteSpace($Pat)) + { + return $true + } + + return (-not([System.String]::IsNullOrWhiteSpace($Pat)) -or $Pat.Length -ne 52) # Note: 52 is the current/expected length of PAT } diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.ps1 index d393daff2..70e13d22d 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.ps1 @@ -37,9 +37,13 @@ function Test-AzDevOpsProjectDescription $IsValid ) - return !($ProjectDescription -eq $null -or - ($ProjectDescription.Contains('%') -or - $ProjectDescription.Contains('*') -or - $ProjectDescription.StartsWith(' ') -or - $ProjectDescription.EndsWith(' '))) + return ( + [String]::IsNullOrWhiteSpace($ProjectDescription) -or + ( + $ProjectDescription.Contains('%') -or + $ProjectDescription.Contains('*') -or + $ProjectDescription.StartsWith(' ') -or + $ProjectDescription.EndsWith(' ') + ) + ) } diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.ps1 index b9991873c..feb429f89 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.ps1 @@ -40,9 +40,9 @@ function Test-AzDevOpsProjectName $AllowWildcard ) - return !([System.String]::IsNullOrWhiteSpace($ProjectName) -or + return -not([System.String]::IsNullOrWhiteSpace($ProjectName) -or ($ProjectName.Contains('%') -or - (!$AllowWildcard -and $ProjectName.Contains('*')) -or + (-not($AllowWildcard) -and $ProjectName.Contains('*')) -or $ProjectName.StartsWith(' ') -or $ProjectName.EndsWith(' '))) } diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-ObjectProperty.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-ObjectProperty.ps1 new file mode 100644 index 000000000..2808f2c35 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-ObjectProperty.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS +Checks if a property exists in an object. + +.DESCRIPTION +The Test-ObjectProperty function checks if a specified property exists in an object. It supports checking properties in hashtables, PSCustomObjects, and PSObjects. + +.PARAMETER Object +The object to check for the existence of the property. + +.PARAMETER PropertyName +The name of the property to check for. + +.OUTPUTS +System.Boolean +Returns $true if the property exists in the object, otherwise returns $false. + +.EXAMPLE +$object = [PSCustomObject]@{ + Name = "John" + Age = 30 +} + +Test-ObjectProperty -Object $object -PropertyName "Name" +# Returns $true + +Test-ObjectProperty -Object $object -PropertyName "Email" +# Returns $false +#> + +Function Test-ObjectProperty +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $Object, + + [Parameter(Mandatory = $true)] + [System.String] + $PropertyName + ) + + # If the object is a hashtable, check if the key exists + if ($Object -is [System.Collections.Hashtable]) + { + return $Object.ContainsKey($PropertyName) + } + # If the object is a PSCustomObject, check if the property exists + elseif ($Object -is [PSCustomObject]) + { + return $Object.PSObject.Properties.Name -contains $PropertyName + } + # If the object is a PSObject, check if the property exists + elseif ($Object -is [PSObject]) + { + return $Object.PSObject.Properties.Name -contains $PropertyName + } + + # Return false + return $false +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.ps1 index 1fa4916bf..35c978c02 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.ps1 @@ -109,7 +109,7 @@ function Wait-AzDevOpsOperation $testOperationParameters.IsSuccessful = $IsSuccessful } - while (!(Test-AzDevOpsOperation @testOperationParameters)) + while (-not(Test-AzDevOpsOperation @testOperationParameters)) { Start-Sleep -Milliseconds $WaitIntervalMilliseconds diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Get-AzDoGitPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Get-AzDoGitPermission.ps1 new file mode 100644 index 000000000..8f7f86310 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Get-AzDoGitPermission.ps1 @@ -0,0 +1,190 @@ +<# +.SYNOPSIS +Retrieves the Git repository permissions for a specified Azure DevOps project and repository. + +.DESCRIPTION +The Get-AzDoGitPermission function retrieves the Git repository permissions for a specified Azure DevOps project and repository. +It performs a lookup within the cache for the repository and retrieves the Access Control List (ACL) for the repository. +The function then compares the retrieved ACLs with the provided permissions and returns the result. + +.PARAMETER ProjectName +The name of the Azure DevOps project. + +.PARAMETER RepositoryName +The name of the Git repository within the Azure DevOps project. + +.PARAMETER isInherited +A boolean value indicating whether the permissions are inherited. + +.PARAMETER Permissions +An optional hashtable array of permissions to compare against the retrieved ACLs. + +.PARAMETER LookupResult +An optional hashtable to store the lookup result. + +.PARAMETER Ensure +An optional parameter to specify the desired state of the permissions. + +.PARAMETER Force +A switch parameter to force the operation. + +.EXAMPLE +Get-AzDoGitPermission -ProjectName "MyProject" -RepositoryName "MyRepo" -isInherited $true + +This example retrieves the Git repository permissions for the "MyRepo" repository in the "MyProject" Azure DevOps project, +considering inherited permissions. + +.NOTES +The function relies on cached items for the repository and security namespace. +It uses helper functions like Get-CacheItem, Get-DevOpsACL, ConvertTo-FormattedACL, ConvertTo-ACL, and Test-ACLListforChanges. + +#> + +Function Get-AzDoGitPermission +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param ( + [Parameter(Mandatory = $true)] + [string]$ProjectName, + + [Parameter(Mandatory = $true)] + [string]$RepositoryName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[Get-AzDoGitPermission] Started." + + # Define the Descriptor Type and Organization Name + $SecurityNamespace = 'Git Repositories' + $OrganizationName = $Global:DSCAZDO_OrganizationName + + Write-Verbose "[Get-AzDoGitPermission] Security Namespace: $SecurityNamespace" + Write-Verbose "[Get-AzDoGitPermission] Organization Name: $OrganizationName" + + # + # Construct a hashtable detailing the group + + $getGroupResult = @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + project = $ProjectName + repositoryName = $RepositoryName + status = $null + reason = $null + } + + Write-Verbose "[Get-AzDoGitPermission] Group result hashtable constructed." + Write-Verbose "[Get-AzDoGitPermission] Performing lookup of permissions for the repository." + + # Define the ACL List + $ACLList = [System.Collections.Generic.List[Hashtable]]::new() + + # + # Perform a Lookup within the Cache for the Repository + $repository = Get-CacheItem -Key $('{0}\{1}' -f $ProjectName, $RepositoryName) -Type 'LiveRepositories' + + # Test if the Repository was found + if (-not $repository) + { + Write-Warning "[Get-AzDoGitPermission] Repository not found: $RepositoryName" + $getGroupResult.status = [DSCGetSummaryState]::NotFound + return $getGroupResult + } + + # + # Perform Lookup of the Permissions for the Repository + + $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces' + Write-Verbose "[Get-AzDoGitPermission] Retrieved namespace: $($namespace.namespaceId)" + + # Add to the ACL Lookup Params + $getGroupResult.namespace = $namespace + + $ACLLookupParams = @{ + OrganizationName = $OrganizationName + SecurityDescriptorId = $namespace.namespaceId + } + + # Get the ACL List and format the ACLS + Write-Verbose "[Get-AzDoGitPermission] ACL Lookup Params: $($ACLLookupParams | Out-String)" + + # Get the ACLs for the Repository + $DevOpsACLs = Get-DevOpsACL @ACLLookupParams + + # Test if the ACLs were found + if ($DevOpsACLs -eq $null) + { + Write-Warning "[Get-AzDoGitPermission] No ACLs found for the repository." + $getGroupResult.status = [DSCGetSummaryState]::NotFound + return $getGroupResult + } + + # Convert the ACLs to a formatted ACL + $DifferenceACLs = $DevOpsACLs | ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName + + # Test if the ACLs were found + if ($DifferenceACLs -eq $null) + { + Write-Warning "[Get-AzDoGitPermission] No ACLs found for the repository." + $getGroupResult.status = [DSCGetSummaryState]::NotFound + return $getGroupResult + } + + $DifferenceACLs = $DifferenceACLs | Where-Object { + ($_.Token.Type -eq 'GitRepository') -and ($_.Token.RepoId -eq $repository.id) + } + + Write-Verbose "[Get-AzDoGitPermission] ACL List retrieved and formatted." + + # + # Convert the Permissions into an ACL Token + + $params = @{ + Permissions = $Permissions + SecurityNamespace = $SecurityNamespace + isInherited = $isInherited + OrganizationName = $OrganizationName + TokenName = '[{0}]\{1}' -f $ProjectName, $RepositoryName + } + + # Convert the Permissions to an ACL Token + $ReferenceACLs = ConvertTo-ACL @params | Where-Object { $_.token.Type -ne 'GitUnknown' } + + # Compare the Reference ACLs to the Difference ACLs + $compareResult = Test-ACLListforChanges -ReferenceACLs $ReferenceACLs -DifferenceACLs $DifferenceACLs + $getGroupResult.propertiesChanged = $compareResult.propertiesChanged + $getGroupResult.status = [DSCGetSummaryState]::"$($compareResult.status)" + $getGroupResult.reason = $compareResult.reason + + Write-Verbose "[Get-AzDoGitPermission] ACL Token converted." + Write-Verbose "[Get-AzDoGitPermission] ACL Token Comparison Result: $($getGroupResult.status)" + + # Export the ACL List to a file + $getGroupResult.ReferenceACLs = $ReferenceACLs + $getGroupResult.DifferenceACLs = $DifferenceACLs + + # Write + Write-Verbose "[Get-AzDoGitPermission] Result Status: $($getGroupResult.status)" + Write-Verbose "[Get-AzDoGitPermission] Returning Group Result." + + # Return the Group Result + return $getGroupResult + +} + diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/New-AzDoGitPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/New-AzDoGitPermission.ps1 new file mode 100644 index 000000000..88eee322d --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/New-AzDoGitPermission.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS +Creates new Git repository permissions in Azure DevOps. + +.DESCRIPTION +The New-AzDoGitPermission function sets up new permissions for a specified Git repository within a given project in Azure DevOps. It uses cached security namespace and project information to serialize ACLs and apply the permissions. + +.PARAMETER ProjectName +The name of the Azure DevOps project. + +.PARAMETER RepositoryName +The name of the Git repository within the Azure DevOps project. + +.PARAMETER isInherited +Indicates whether the permissions are inherited. + +.PARAMETER Permissions +A hashtable array of permissions to be applied. + +.PARAMETER LookupResult +A hashtable containing the lookup result properties. + +.PARAMETER Ensure +Specifies whether to ensure the permissions are set. + +.PARAMETER Force +A switch parameter to force the operation. + +.EXAMPLE +New-AzDoGitPermission -ProjectName "MyProject" -RepositoryName "MyRepo" -isInherited $true -Permissions $permissions -LookupResult $lookupResult -Ensure "Present" -Force + +.NOTES +This function relies on cached items for security namespace and project information. Ensure that the cache is populated before calling this function. +#> +Function New-AzDoGitPermission +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$ProjectName, + + [Parameter(Mandatory = $true)] + [string]$RepositoryName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[New-AzDoGitPermission] Started." + + # + # Security Namespace ID + + $SecurityNamespace = Get-CacheItem -Key 'Git Repositories' -Type 'SecurityNamespaces' + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + if (($null -eq $SecurityNamespace) -or ($null -eq $Project)) + { + Write-Warning "[New-AzDoGitPermission] Security Namespace or Project not found." + return + } + + # + # Serialize the ACLs + + $serializeACLParams = @{ + ReferenceACLs = $LookupResult.propertiesChanged + DescriptorACLList = Get-CacheItem -Key $SecurityNamespace.namespaceId -Type 'LiveACLList' + DescriptorMatchToken = ($LocalizedDataAzSerializationPatten.GitRepository -f $Project.id) + } + + $params = @{ + OrganizationName = $Global:DSCAZDO_OrganizationName + SecurityNamespaceID = $SecurityNamespace.namespaceId + SerializedACLs = ConvertTo-ACLHashtable @serializeACLParams + } + + # + # Set the Git Repository Permissions + + Set-AzDoPermission @params + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Remove-AzDoGitPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Remove-AzDoGitPermission.ps1 new file mode 100644 index 000000000..3683b6233 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Remove-AzDoGitPermission.ps1 @@ -0,0 +1,95 @@ +Function Remove-AzDoGitPermission +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$ProjectName, + + [Parameter(Mandatory = $true)] + [string]$RepositoryName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[New-AzDoGitPermission] Started." + + # + # Security Namespace ID + + # Get the Security Namespace + $SecurityNamespace = Get-CacheItem -Key 'Git Repositories' -Type 'SecurityNamespaces' + + # If the Security Namespace is null, return + if (-not $SecurityNamespace) + { + Write-Error "[New-AzDoGitPermission] Security Namespace not found." + return + } + + # Get the Project + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + # If the Project is null, return + if (-not $Project) + { + Write-Error "[New-AzDoGitPermission] Project not found." + return + } + + # Get the Repository + $Repository = Get-CacheItem -Key "$ProjectName\$RepositoryName" -Type 'LiveRepositories' + + # If the Repository is null, return + if (-not $Repository) + { + Write-Error "[New-AzDoGitPermission] Repository not found." + return + } + + # Get the ACLs + $DescriptorACLList = Get-CacheItem -Key $SecurityNamespace.namespaceId -Type 'LiveACLList' + + # If the ACLs are null, return + if (-not $DescriptorACLList) + { + Write-Error "[New-AzDoGitPermission] ACLs not found." + return + } + + # + # Filter the ACLs that pertain to the Git Repository + + $searchString = 'repoV2/{0}/{1}' -f $Project.id, $Repository.id + + # Test if the Token exists + $Filtered = $DescriptorACLList | Where-Object { $_.token -eq $searchString } + + # If the ACLs are not null, remove them + if ($Filtered) + { + $params = @{ + OrganizationName = $Global:DSCAZDO_OrganizationName + SecurityNamespaceID = $SecurityNamespace.namespaceId + TokenName = $searchString + } + + # Remove the ACLs + Remove-AzDoPermission @params + + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Set-AzDoGitPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Set-AzDoGitPermission.ps1 new file mode 100644 index 000000000..947b024ad --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Set-AzDoGitPermission.ps1 @@ -0,0 +1,68 @@ +Function Set-AzDoGitPermission +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$ProjectName, + + [Parameter(Mandatory = $true)] + [string]$RepositoryName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[Set-AzDoPermission] Started." + + # + # Security Namespace ID + + $SecurityNamespace = Get-CacheItem -Key 'Git Repositories' -Type 'SecurityNamespaces' + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + if ($SecurityNamespace -eq $null) + { + Write-Error "[Set-AzDoPermission] Security Namespace not found." + return + } + + if ($Project -eq $null) + { + Write-Error "[Set-AzDoPermission] Project not found." + return + } + + # + # Serialize the ACLs + + $serializeACLParams = @{ + ReferenceACLs = $LookupResult.propertiesChanged + DescriptorACLList = Get-CacheItem -Key $SecurityNamespace.namespaceId -Type 'LiveACLList' + DescriptorMatchToken = ($LocalizedDataAzSerializationPatten.GitRepository -f $Project.id) + } + + $params = @{ + OrganizationName = $Global:DSCAZDO_OrganizationName + SecurityNamespaceID = $SecurityNamespace.namespaceId + SerializedACLs = ConvertTo-ACLHashtable @serializeACLParams + } + + # + # Set the Git Repository Permissions + + Set-AzDoPermission @params + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Get-AzDoGitRepository.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Get-AzDoGitRepository.ps1 new file mode 100644 index 000000000..de5a2c091 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Get-AzDoGitRepository.ps1 @@ -0,0 +1,103 @@ +<# +.SYNOPSIS +Retrieves an Azure DevOps Git repository from the live and local cache. + +.DESCRIPTION +The Get-AzDoGitRepository function attempts to retrieve an Azure DevOps Git repository based on the provided project and repository names. It first checks the live cache for the repository and returns the repository object if found. If the repository is not found in the live cache, it returns a status indicating that the repository was not found. + +.PARAMETER ProjectName +The name of the Azure DevOps project. + +.PARAMETER RepositoryName +The name of the Azure DevOps Git repository. + +.PARAMETER SourceRepository +(Optional) The source repository name. + +.PARAMETER LookupResult +(Optional) A hashtable to store lookup results. + +.PARAMETER Ensure +(Optional) Specifies the desired state of the repository. + +.PARAMETER Force +(Optional) A switch parameter to force the operation. + +.OUTPUTS +System.Management.Automation.PSObject[] +Returns a hashtable detailing the repository status and properties. + +.EXAMPLE +PS C:\> Get-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" + +This command retrieves the "MyRepo" repository from the "MyProject" project. + +#> +Function Get-AzDoGitRepository +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter(Mandatory = $true)] + [Alias('Repository')] + [System.String]$RepositoryName, + + [Parameter()] + [Alias('Source')] + [System.String]$SourceRepository, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # + # Construct a hashtable detailing the group + + $getRepositoryResult = @{ + #Reasons = $() + Ensure = [Ensure]::Absent + liveCache = $livegroup + propertiesChanged = @() + status = $null + } + + # + # Attempt to retrive the Project Group from the Live and Local Cache. + Write-Verbose "[Get-AzDoGitRepository] Retriving the Project Group from the Live and Local Cache." + + # Format the Key for the Project Group. + $projectGroupKey = "$ProjectName\$RepositoryName" + + # Retrive the Repositories from the Live Cache. + $repository = Get-CacheItem -Key $projectGroupKey -Type 'LiveRepositories' + + # If the Repository exists in the Live Cache, return the Repository object. + if ($repository) + { + Write-Verbose "[Get-AzDoGitRepository] The Repository '$RepositoryName' was found in the Live Cache." + $getRepositoryResult.status = [DSCGetSummaryState]::Unchanged + return $getRepositoryResult + + } + else + { + Write-Verbose "[Get-AzDoGitRepository] The Repository '$RepositoryName' was not found in the Live Cache." + $getRepositoryResult.status = [DSCGetSummaryState]::NotFound + } + + # Return the Repository object. + return $getRepositoryResult + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/New-AzDoGitRepository.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/New-AzDoGitRepository.ps1 new file mode 100644 index 000000000..0dffb89af --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/New-AzDoGitRepository.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS +Creates a new Azure DevOps Git repository within a specified project. + +.DESCRIPTION +The New-AzDoGitRepository function creates a new Git repository in an Azure DevOps project. +It uses the provided project name and repository name to create the repository. +Optionally, a source repository can be specified to initialize the new repository. + +.PARAMETER ProjectName +The name of the Azure DevOps project where the new repository will be created. + +.PARAMETER RepositoryName +The name of the new Git repository to be created. + +.PARAMETER SourceRepository +(Optional) The name of the source repository to initialize the new repository. + +.PARAMETER LookupResult +(Optional) A hashtable to store lookup results. + +.PARAMETER Ensure +(Optional) Specifies whether to ensure the repository exists or does not exist. + +.PARAMETER Force +(Optional) Forces the creation of the repository even if it already exists. + +.EXAMPLE +PS> New-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" + +Creates a new Git repository named "MyRepo" in the "MyProject" Azure DevOps project. + +.EXAMPLE +PS> New-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" -SourceRepository "TemplateRepo" + +Creates a new Git repository named "MyRepo" in the "MyProject" Azure DevOps project, initialized with the contents of "TemplateRepo". + +.NOTES +This function requires the Azure DevOps organization name to be set in the global variable $Global:DSCAZDO_OrganizationName. +#> + +Function New-AzDoGitRepository +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter(Mandatory = $true)] + [Alias('Repository')] + [System.String]$RepositoryName, + + [Parameter()] + [Alias('Source')] + [System.String]$SourceRepository, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[New-AzDoGitRepository] Creating new repository '$($RepositoryName)' in project '$($ProjectName)'" + + # Define parameters for creating a new DevOps group + $params = @{ + ApiUri = 'https://dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + RepositoryName = $RepositoryName + SourceRepository = $SourceRepository + } + + if ($null -eq $params.Project) + { + Write-Error "[New-AzDoGitRepository] Project '$($ProjectName)' does not exist in the LiveProjects cache. Skipping change." + return + } + + + # Create a new repository + $value = New-GitRepository @params + + # Add the repository to the LiveRepositories cache and write to verbose log + Add-CacheItem -Key "$ProjectName\$RepositoryName" -Value $value -Type 'LiveRepositories' + Export-CacheObject -CacheType 'LiveRepositories' -Content $AzDoLiveRepositories + Refresh-CacheObject -CacheType 'LiveRepositories' + Write-Verbose "[New-AzDoGitRepository] Added new group to LiveGroups cache with key: '$($value.Name)'" + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Remove-AzDoGitRepository.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Remove-AzDoGitRepository.ps1 new file mode 100644 index 000000000..bb150d04f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Remove-AzDoGitRepository.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS +Removes a Git repository from an Azure DevOps project. + +.DESCRIPTION +The Remove-AzDoGitRepository function removes a specified Git repository from a given Azure DevOps project. +It checks the existence of the project and repository in the LiveProjects and LiveRepositories cache before attempting the removal. + +.PARAMETER ProjectName +The name of the Azure DevOps project containing the repository to be removed. + +.PARAMETER RepositoryName +The name of the repository to be removed. + +.PARAMETER SourceRepository +An optional parameter specifying the source repository. + +.PARAMETER LookupResult +An optional hashtable parameter for lookup results. + +.PARAMETER Ensure +An optional parameter to ensure the state of the repository. + +.PARAMETER Force +A switch parameter to force the removal of the repository. + +.EXAMPLE +Remove-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" -Force + +.NOTES +This function relies on the existence of certain global variables and cache items. +Ensure that the necessary cache items and global variables are properly set before invoking this function. +#> +Function Remove-AzDoGitRepository +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter(Mandatory = $true)] + [Alias('Repository')] + [System.String]$RepositoryName, + + [Parameter()] + [Alias('Source')] + [System.String]$SourceRepository, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[Remove-AzDoGitRepository] Removing repository '$($RepositoryName)' in project '$($ProjectName)'" + + # Define parameters for creating a new DevOps group + $params = @{ + ApiUri = 'https://dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + Repository = Get-CacheItem -Key "$ProjectName\$RepositoryName" -Type 'LiveRepositories' + } + + # Check if the project exists in the LiveProjects cache + if (($null -eq $params.Project) -or ($null -eq $params.Repository)) + { + Write-Error "[Remove-AzDoGitRepository] Project '$($ProjectName)' or Repository '$($RepositoryName)' does not exist in the LiveProjects or LiveRepositories cache. Skipping change." + return + } + + # Create a new repository + $value = Remove-GitRepository @params + + # Add the repository to the LiveRepositories cache and write to verbose log + Remove-CacheItem -Key "$ProjectName\$RepositoryName" -Type 'LiveRepositories' + Export-CacheObject -CacheType 'LiveRepositories' -Content $AzDoLiveRepositories + Write-Verbose "[Remove-AzDoGitRepository] Added new group to LiveGroups cache with key: '$($value.Name)'" + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Set-AzDoGitRepository.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Set-AzDoGitRepository.ps1 new file mode 100644 index 000000000..068ce60e9 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Set-AzDoGitRepository.ps1 @@ -0,0 +1,67 @@ +<# +.SYNOPSIS +Sets the configuration for an Azure DevOps Git repository. + +.DESCRIPTION +The Set-AzDoGitRepository function configures an Azure DevOps Git repository based on the provided parameters. It allows specifying the project name, repository name, source repository, and other optional parameters. + +.PARAMETER ProjectName +The name of the Azure DevOps project. This parameter is mandatory. + +.PARAMETER RepositoryName +The name of the Azure DevOps Git repository. This parameter is mandatory. + +.PARAMETER SourceRepository +The name of the source repository to use for configuration. This parameter is optional. + +.PARAMETER LookupResult +A hashtable containing lookup results. This parameter is optional. + +.PARAMETER Ensure +Specifies whether the repository should be present or absent. This parameter is optional. + +.PARAMETER Force +A switch parameter to force the operation. This parameter is optional. + +.OUTPUTS +[System.Management.Automation.PSObject[]] +Returns an array of PSObject representing the result of the operation. + +.EXAMPLE +Set-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" -SourceRepository "SourceRepo" + +.EXAMPLE +Set-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" -Force +#> +Function Set-AzDoGitRepository +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter(Mandatory = $true)] + [Alias('Repository')] + [System.String]$RepositoryName, + + [Parameter()] + [Alias('Source')] + [System.String]$SourceRepository, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Skipped + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Get-AzDoGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Get-AzDoGroupMember.ps1 new file mode 100644 index 000000000..5b78239b0 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Get-AzDoGroupMember.ps1 @@ -0,0 +1,182 @@ +<# +.SYNOPSIS +Retrieves the members of an Azure DevOps group and compares them with the provided parameters. + +.DESCRIPTION +The Get-AzDoGroupMember function retrieves the members of an Azure DevOps group from the live cache and compares them with the provided parameters. It returns a hashtable detailing the group status, including any differences between the live group members and the provided parameters. + +.PARAMETER GroupName +The name of the Azure DevOps group to retrieve. + +.PARAMETER GroupMembers +An array of group members to compare against the live group members. Default is an empty array. + +.PARAMETER LookupResult +A hashtable to store lookup results. + +.PARAMETER Ensure +Specifies whether the group should be present or absent. + +.PARAMETER Force +A switch parameter to force the operation. + +.OUTPUTS +System.Management.Automation.PSObject[] +A hashtable detailing the group status, including any differences between the live group members and the provided parameters. + +.EXAMPLE +PS C:\> Get-AzDoGroupMember -GroupName "Developers" -GroupMembers @("user1", "user2") + +This command retrieves the members of the "Developers" group and compares them with the provided members "user1" and "user2". + +.NOTES +This function uses verbose logging to provide detailed information about its operations. +#> + +Function Get-AzDoGroupMember +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Members')] + [System.String[]]$GroupMembers=@(), + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Logging + Write-Verbose "[Get-AzDoGroupMember] Retriving the GroupName from the Live and Local Cache." + + # Format the According to the Group Name + $Key = Format-AzDoGroupMember -GroupName $GroupName + + # Check the cache for the group + $livegroupMembers = Get-CacheItem -Key $Key -Type 'LiveGroupMembers' + + Write-Verbose "[Get-AzDoGroupMember] GroupName: '$GroupName'" + + # + # Construct a hashtable detailing the group + $getGroupResult = @{ + #Reasons = $() + Ensure = [Ensure]::Absent + groupName = $GroupName + reference = $GroupMembers + difference = $livegroupMembers + propertiesChanged = @() + status = $null + } + + Write-Verbose "[Get-AzDoGroupMember] Testing LocalCache, LiveCache and Parameters." + + # + # Test if the group is present in the live cache + if ($null -eq $livegroupMembers) + { + Write-Verbose "[Get-AzDoGroupMember] Group '$GroupName' not found in the live cache." + # If there are no group members, test to see if there are group members defined in the parameters + if ($GroupMembers.Count -eq 0) + { + $getGroupResult.status = [DSCGetSummaryState]::Unchanged + } + else + { + # If there are group members defined in the parameters, but no live group members, the group is new. + $getGroupResult.status = [DSCGetSummaryState]::NotFound + } + + # Return the result + return $getGroupResult + } + + # + # Test if there are no group members in parameters + if ($GroupMembers.Count -eq 0) + { + Write-Verbose "[Get-AzDoGroupMember] Group '$GroupName' not found in the parameters." + + # If there are no live group members, the group is unchanged. + if ($livegroupMembers.Count -eq 0) + { + $getGroupResult.status = [DSCGetSummaryState]::Unchanged + } + else + { + # If there are live group members, the groups members are to be removed. + $getGroupResult.status = [DSCGetSummaryState]::Missing + } + + # Return the result + return $getGroupResult + } + + # + # If both parameters and group members exist. + + # Compare the members of the live group with the parameters. + + # Format the parameters + $FormattedLiveGroups = @($livegroupMembers) + $FormattedParametersGroups = $GroupMembers | ForEach-Object { Find-AzDoIdentity $_ } + + # If the formatted live groups is empty. Modify the formatted live groups to be an empty array. + if ($null -eq $FormattedLiveGroups) + { + $FormattedLiveGroups = @() + } + + # + # Compare the live group members with the parameters + + $params = @{ + ReferenceObject = $FormattedParametersGroups + DifferenceObject = $FormattedLiveGroups + Property = 'originId' + } + + # + # Compare the group members + $members = Compare-Object @Params -ErrorAction SilentlyContinue + + # + # If there are no differences, the group is unchanged. + + if ($members.Count -eq 0) + { + # The group is unchanged. + $getGroupResult.status = [DSCGetSummaryState]::Unchanged + } + else + { + # Users on the left side are in the comparison object but not in the reference object are to be added. + # Users on the right side are in the reference object but not in the comparison object are to be removed. + $getGroupResult.propertiesChanged += $members | ForEach-Object { + $originId = $_.originId + @{ + action = ($_.SideIndicator -eq '<=') ? 'Add' : 'Remove' + value = ($FormattedParametersGroups | Where-Object { $_.originId -eq $originId }) ?? + ($FormattedLiveGroups | Where-Object { $_.originId -eq $originId }) + + } + } + # The group has changed. + $getGroupResult.status = [DSCGetSummaryState]::Changed + } + + return $getGroupResult + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/New-AzDoGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/New-AzDoGroupMember.ps1 new file mode 100644 index 000000000..f99513f7f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/New-AzDoGroupMember.ps1 @@ -0,0 +1,143 @@ +<# +.SYNOPSIS + Adds members to an Azure DevOps group. + +.DESCRIPTION + The New-AzDoGroupMember function adds specified members to an Azure DevOps group. + It fetches the group identity, retrieves cached group members, and performs a lookup + for each member to add them to the group. The function also handles circular references + and updates the cache with the new group members. + +.PARAMETER GroupName + The name of the Azure DevOps group to which members will be added. + +.PARAMETER GroupMembers + An array of member identities to be added to the group. Default is an empty array. + +.PARAMETER LookupResult + A hashtable containing lookup results. + +.PARAMETER Ensure + Specifies whether to ensure the presence or absence of the group members. + +.PARAMETER Force + A switch parameter to force the addition of members even if they are already cached. + +.EXAMPLE + PS> New-AzDoGroupMember -GroupName "Developers" -GroupMembers "user1", "user2" + + This example adds "user1" and "user2" to the "Developers" group. + +.NOTES + The function writes verbose messages to indicate the progress of the group member addition process. + It also handles errors and warnings for cases such as circular references and missing identities. +#> + +Function New-AzDoGroupMember +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Members')] + [System.String[]]$GroupMembers=@(), + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Write a verbose log message indicating that the function has started executing. + Write-Verbose "[New-AzDoGroupMember] Starting group member addition process for group '$GroupName'." + + # Fetch the Group Identity + $GroupIdentity = Find-AzDoIdentity $GroupName + Write-Verbose "[New-AzDoGroupMember] Fetched group identity for '$GroupName'." + + # Retrieve the group members from the cache + $CachedGroupMembers = Get-CacheObject -CacheType 'LiveGroupMembers' + Write-Verbose "[New-AzDoGroupMember] Retrieved cached group members." + + # Check if the group members are already cached + if ( + ($null -ne $CachedGroupMembers) -and + (($CachedGroupMembers | Where-Object { $_.Key -eq $GroupIdentity.principalName }).Count -ne 0) + ) + { + Write-Error "[New-AzDoGroupMember] Group members are already cached for group '$GroupName'." + return + } + + $params = @{ + GroupIdentity = $GroupIdentity + ApiUri = 'https://vssps.dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + } + + Write-Verbose "[New-AzDoGroupMember] Starting group member addition process for group '$GroupName'." + Write-Verbose "[New-AzDoGroupMember] Group members: $($GroupMembers -join ',')." + + # Define the members + $members = [System.Collections.Generic.List[object]]::new() + + # Fetch the group members and perform a lookup of the members + ForEach ($MemberIdentity in $GroupMembers) + { + # Use the Find-AzDoIdentity function to search for an Azure DevOps identity that matches the given $MemberIdentity. + Write-Verbose "[New-AzDoGroupMember] Looking up identity for member '$MemberIdentity'." + $identity = Find-AzDoIdentity -Identity $MemberIdentity + + # If the identity is not found, write a warning message to the console and continue to the next member. + if ($null -eq $identity) + { + Write-Warning "[New-AzDoGroupMember] Unable to find identity for member '$MemberIdentity'." + continue + } + + # Check for circular reference + if ($GroupIdentity.originId -eq $identity.originId) + { + Write-Warning "[New-AzDoGroupMember] Circular reference detected for member '$MemberIdentity'." + continue + } + + Write-Verbose "[New-AzDoGroupMember] Found identity for member '$MemberIdentity'." + + # Call the New-DevOpsGroupMember function with a hashtable of parameters to add the found identity as a new member to a group. + Write-Verbose "[New-AzDoGroupMember] Adding member '$MemberIdentity' to group '$($params.GroupIdentity.displayName)'." + + $result = New-DevOpsGroupMember @params -MemberIdentity $identity + + # Add the member to the list + $members.Add($identity) + Write-Verbose "[New-AzDoGroupMember] Member '$MemberIdentity' added to the internal list." + } + + # If the group members are not found, write a warning message to the console and return. + if ($members.Count -eq 0) + { + Write-Warning "[New-AzDoGroupMember] No group members found: $($GroupMembers -join ',')." + return + } + + # Add the group to the cache + Write-Verbose "[New-AzDoGroupMember] Added group '$GroupName' with members to the cache." + Add-CacheItem -Key $GroupIdentity.principalName -Value $members -Type 'LiveGroupMembers' + + Write-Verbose "[New-AzDoGroupMember] Updated global cache with live group information." + Set-CacheObject -Content $Global:AzDoLiveGroupMembers -CacheType 'LiveGroupMembers' + + # Write a verbose log message indicating that the function has completed the group member addition process. + Write-Verbose "[New-AzDoGroupMember] Completed group member addition process for group '$GroupName'." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Remove-AzDoGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Remove-AzDoGroupMember.ps1 new file mode 100644 index 000000000..52cf5eff5 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Remove-AzDoGroupMember.ps1 @@ -0,0 +1,119 @@ +<# +.SYNOPSIS +Removes members from an Azure DevOps group. + +.DESCRIPTION +The Remove-AzDoGroupMember function removes specified members from an Azure DevOps group. +It looks up the group identity, checks the cache for existing group members, and removes the specified members. + +.PARAMETER GroupName +The name of the Azure DevOps group from which members will be removed. + +.PARAMETER GroupMembers +An array of group members to be removed. This parameter is optional. + +.PARAMETER LookupResult +A hashtable containing lookup results. This parameter is optional. + +.PARAMETER Ensure +Specifies whether to ensure the removal of the group members. This parameter is optional. + +.PARAMETER Force +A switch parameter to force the removal of group members without confirmation. + +.EXAMPLE +Remove-AzDoGroupMember -GroupName "Developers" -GroupMembers "user1@example.com", "user2@example.com" + +This command removes the specified members from the "Developers" group. + +.EXAMPLE +Remove-AzDoGroupMember -GroupName "Developers" -Force + +This command forces the removal of all members from the "Developers" group without confirmation. + +.NOTES +This function requires the Azure DevOps module and appropriate permissions to manage group memberships. + +#> +Function Remove-AzDoGroupMember +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Members')] + [System.String[]]$GroupMembers=@(), + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Group Identity + $GroupIdentity = Find-AzDoIdentity $GroupName + + # Format the According to the Group Name + $Key = Format-AzDoProjectName -GroupName $GroupName -OrganizationName $Global:DSCAZDO_OrganizationName + + # Check the cache for the group + $LiveGroupMembers = @(Get-CacheItem -Key $Key -Type 'LiveGroupMembers') + + # If the group identity or key is not found, write a warning message to the console and return. + if ([String]::IsNullOrEmpty($GroupIdentity) -or [String]::IsNullOrWhiteSpace($GroupIdentity)) + { + Write-Warning "[Remove-AzDoGroupMember] Unable to find identity for group '$GroupName'." + return + } + + $params = @{ + GroupIdentity = $GroupIdentity + ApiUri = 'https://vssps.dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + } + + Write-Verbose "[Remove-AzDoGroupMember] Starting group member removal process for group '$GroupName'." + Write-Verbose "[Remove-AzDoGroupMember] Group members: $($LiveGroupMembers.principalName -join ',')." + + # Fetch the group members and perform a lookup of the members + ForEach ($MemberIdentity in $LiveGroupMembers) + { + + # Use the Find-AzDoIdentity function to search for an Azure DevOps identity that matches the given $MemberIdentity. + Write-Verbose "[Remove-AzDoGroupMember] Looking up identity for member '$($MemberIdentity.principalName)'." + $identity = Find-AzDoIdentity -Identity $MemberIdentity.principalName + + # If the identity is not found, write a warning message to the console and continue to the next member. + if ([String]::IsNullOrEmpty($identity) -or [String]::IsNullOrWhiteSpace($identity)) + { + Write-Warning "[Remove-AzDoGroupMember] Unable to find identity for member '$($MemberIdentity.principalName)'." + continue + } + + # Call the New-DevOpsGroupMember function with a hashtable of parameters to add the found identity as a new member to a group. + Write-Verbose "[Remove-AzDoGroupMember] Removing member '$($MemberIdentity.principalName)' from group '$($params.GroupIdentity.displayName)'." + + $result = Remove-DevOpsGroupMember @params -MemberIdentity $identity + + } + + # Add the group to the cache + Write-Verbose "[Remove-AzDoGroupMember] Removed group '$GroupName' with members to the cache." + Remove-CacheItem -Key $GroupIdentity.principalName -Type 'LiveGroupMembers' + + Write-Verbose "[Remove-AzDoGroupMember] Updated global cache with live group information." + Set-CacheObject -Content $Global:AzDoLiveGroupMembers -CacheType 'LiveGroupMembers' + + # Write a verbose log message indicating that the function has completed the group member removal process. + Write-Verbose "[Remove-AzDoGroupMember] Completed group member removal process for group '$GroupName'." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Set-AzDoGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Set-AzDoGroupMember.ps1 new file mode 100644 index 000000000..2af6ac5c7 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Set-AzDoGroupMember.ps1 @@ -0,0 +1,161 @@ +<# +.SYNOPSIS + Manages Azure DevOps group members by adding or removing members based on the provided lookup result. + +.PARAMETER GroupName + The name of the Azure DevOps group to manage. + +.PARAMETER GroupMembers + An array of group members to be managed. Defaults to an empty array. + +.PARAMETER LookupResult + A hashtable containing the propertiesChanged key which indicates the action to be performed (Add or Remove). + +.PARAMETER Ensure + Specifies whether the group members should be present or absent. + +.PARAMETER Force + A switch parameter to force the operation. + +.DESCRIPTION + The Set-AzDoGroupMember function manages Azure DevOps group members by adding or removing members based on the provided lookup result. + It checks for circular references and updates the internal cache with the new group member information. + +.EXAMPLE + Set-AzDoGroupMember -GroupName "Developers" -GroupMembers @("user1", "user2") -LookupResult $lookupResult -Ensure "Present" + + This example adds the specified members to the "Developers" group based on the lookup result. + +.NOTES + The function relies on several helper functions such as Find-AzDoIdentity, Format-AzDoProjectName, Get-CacheItem, New-DevOpsGroupMember, Remove-DevOpsGroupMember, Add-CacheItem, and Set-CacheObject. + Ensure that these functions are defined and available in the scope where Set-AzDoGroupMember is called. + +#> + +Function Set-AzDoGroupMember +{ + param( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Members')] + [System.String[]]$GroupMembers=@(), + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Group Identity + $GroupIdentity = Find-AzDoIdentity $GroupName + + # Format the According to the Group Name + $Key = Format-AzDoProjectName -GroupName $GroupName -OrganizationName $Global:DSCAZDO_OrganizationName + # Check the cache for the group + $members = [System.Collections.ArrayList]::New() + Get-CacheItem -Key $Key -Type 'LiveGroupMembers' | ForEach-Object { $members.Add($_) } + + # If the members are null or empty, stop. + if (($null -eq $GroupMembers) -or ($members.Count -eq 0)) + { + Write-Error "[Set-AzDoGroupMember] No members found in the LiveGroupMembers cache for group '$Key'." + return + } + + # If the lookup result is not provided, we need to look it up. + if ($null -eq $LookupResult.propertiesChanged) + { + Throw "[Set-AzDoGroupMember] - LookupResult.propertiesChanged is required." + } + + # Fetch the Group Identity + $params = @{ + GroupIdentity = $GroupIdentity + ApiUri = 'https://vssps.dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + } + + Write-Verbose "[Set-AzDoGroupMember] Starting group member addition process for group '$GroupName'." + + # If the lookup result is not provided, we need to look it up. + switch ($LookupResult.propertiesChanged) + { + + # Add members + { $_.action -eq "Add" } { + + # Use the Find-AzDoIdentity function to search for an Azure DevOps identity that matches the given $MemberIdentity. + Write-Verbose "[Set-AzDoGroupMember][ADD] Adding Identity for Principal Name '$($_.value.principalName)'." + $identity = $_.value + + # Check for circular reference + if ($GroupIdentity.originId -eq $identity.originId) + { + Write-Warning "[Set-AzDoGroupMember][ADD] Circular reference detected for member '$($GroupIdentity.principalName)'." + continue + } + + # Call the New-DevOpsGroupMember function with a hashtable of parameters to add the found identity as a new member to a group. + Write-Verbose "[Set-AzDoGroupMember][ADD] Adding member '$($identity.displayName)' to group '$($params.GroupIdentity.displayName)'." + + $result = New-DevOpsGroupMember @params -MemberIdentity $identity + + # Add the member to the list + $members.Add($identity) + Write-Verbose "[Set-AzDoGroupMember][ADD] Member '$($identity.displayName)' added to the internal list." + + } + + # Remove + { $_.action -eq "Remove" } { + + # Use the Find-AzDoIdentity function to search for an Azure DevOps identity that matches the given $MemberIdentity. + Write-Verbose "[Set-AzDoGroupMember][REMOVE] Removing Identity for Principal Name '$($_.value.principalName)'." + $identity = $_.value + + # Check for circular reference + if ($GroupIdentity.originId -eq $identity.originId) + { + Write-Warning "[Set-AzDoGroupMember][REMOVE] Circular reference detected for member '$($GroupIdentity.principalName)'." + continue + } + + # Call the New-DevOpsGroupMember function with a hashtable of parameters to add the found identity as a new member to a group. + Write-Verbose "[Set-AzDoGroupMember][REMOVE] Removing member '$($identity.displayName)' to group '$($params.GroupIdentity.displayName)'." + + $result = Remove-DevOpsGroupMember @params -MemberIdentity $identity + + # Remove the member from the list + + Write-Verbose "[Set-AzDoGroupMember][REMOVE] Removing member '$($identity.displayName)' from the internal list." + Write-Verbose "[Set-AzDoGroupMember][REMOVE] members count: $($members.count)" + + $id = 0 .. $members.count | Where-Object { $members[$_].originId -eq $identity.originId } + $members.RemoveAt($id) + Write-Verbose "[Set-AzDoGroupMember][REMOVE] Member '$($identity.displayName)' removed from the internal list." + + } + + # Default + Default { + Write-Warning "[Set-AzDoGroupMember] Invalid action '$($_.action)' provided." + } + + } + + # Add the group to the cache + Write-Verbose "[Set-AzDoGroupMember] Added group '$GroupName' with the updated member list to the cache." + Add-CacheItem -Key $GroupIdentity.principalName -Value $members -Type 'LiveGroupMembers' + + Write-Verbose "[Set-AzDoGroupMember] Updated global cache with live group information." + Set-CacheObject -Content $Global:AzDoLiveGroupMembers -CacheType 'LiveGroupMembers' + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Test-AzDoGroupMember.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Test-AzDoGroupMember.ps1 new file mode 100644 index 000000000..3d186415a --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Test-AzDoGroupMember.ps1 @@ -0,0 +1,48 @@ +<# +.SYNOPSIS + Tests the membership of a specified Azure DevOps group. + +.PARAMETER GroupName + The name of the Azure DevOps group to test. + +.PARAMETER GroupMembers + An array of members to check within the Azure DevOps group. Default is an empty array. + +.PARAMETER LookupResult + A hashtable containing lookup results for the group members. + +.PARAMETER Ensure + Specifies whether the group members should be present or absent. + +.PARAMETER Force + Forces the operation to proceed without prompting for confirmation. + +.NOTES +#> + +Function Test-AzDoGroupMember +{ + param( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Members')] + [System.String[]]$GroupMembers=@(), + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + $return + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Get-AzDoGroupPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Get-AzDoGroupPermission.ps1 new file mode 100644 index 000000000..5c6054739 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Get-AzDoGroupPermission.ps1 @@ -0,0 +1,195 @@ +<# +.SYNOPSIS +Retrieves the permissions for a specified Azure DevOps group. + +.DESCRIPTION +The Get-AzDoGroupPermission function retrieves the permissions for a specified Azure DevOps group. +It performs a lookup within the cache for the group and its associated project, retrieves the +security namespace, and constructs a hashtable detailing the group. It then performs a lookup +of the permissions for the group, formats the ACLs, and compares the reference ACLs to the +difference ACLs to determine any changes. + +.PARAMETER GroupName +The name of the Azure DevOps group. This parameter is mandatory. + +.PARAMETER isInherited +A boolean value indicating whether the permissions are inherited. This parameter is mandatory. + +.PARAMETER Permissions +An array of hashtables representing the permissions to be checked. This parameter is optional. + +.PARAMETER LookupResult +A hashtable representing the lookup result. This parameter is optional. + +.PARAMETER Ensure +Specifies the desired state of the group permissions. This parameter is optional. + +.PARAMETER Force +A switch parameter to force the operation. This parameter is optional. + +.OUTPUTS +System.Management.Automation.PSObject[] +Returns a hashtable detailing the group permissions, including the reference ACLs, difference ACLs, +properties changed, status, and reason. + +.EXAMPLE +PS C:\> Get-AzDoGroupPermission -GroupName "ProjectName\GroupName" -isInherited $true + +Retrieves the permissions for the specified Azure DevOps group with inheritance. + +#> + +Function Get-AzDoGroupPermission +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param ( + [Parameter(Mandatory = $true)] + [string]$GroupName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[Get-AzDoGroupPermission] Started." + + # Define the Descriptor Type and Organization Name + $SecurityNamespace = 'Identity' + $OrganizationName = $Global:DSCAZDO_OrganizationName + # Split the Group Name + $split = $GroupName.Split('\').Split('/') + + # Test if the Group Name is valid + if ($split.Count -ne 2) + { + Write-Warning "[Get-AzDoGroupPermission] Invalid Group Name: $GroupName" + return + } + + # Define the Project and Group Name + $ProjectName = $split[0].Replace('[', '').Replace(']', '') + $GroupName = $split[1] + + # If the Project Name contains 'organization'. Update the Project Name + + Write-Verbose "[Get-AzDoGroupPermission] Security Namespace: $SecurityNamespace" + Write-Verbose "[Get-AzDoGroupPermission] Organization Name: $OrganizationName" + + # + # Construct a hashtable detailing the group + + $getGroupResult = @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + project = $ProjectName + groupName = $GroupName + status = $null + reason = $null + } + + Write-Verbose "[Get-AzDoGroupPermission] Group result hashtable constructed." + Write-Verbose "[Get-AzDoGroupPermission] Performing lookup of permissions for the group." + + # Define the ACL List + $ACLList = [System.Collections.Generic.List[Hashtable]]::new() + + # Perform a Lookup within the Cache for the Group + $group = Get-CacheItem -Key $('[{0}]\{1}' -f $ProjectName, $GroupName) -Type 'LiveGroups' + $project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + # Test if the Group was found + if (-not $group) + { + Throw "[Get-AzDoGroupPermission] Group not found: $('[{0}]\{1}' -f $ProjectName, $GroupName)" + return + } + + # + # Perform Lookup of the Permissions for the Group + + $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces' + Write-Verbose "[Get-AzDoGroupPermission] Retrieved namespace: $($namespace.namespaceId)" + + # Add to the ACL Lookup Params + $getGroupResult.namespace = $namespace + + $ACLLookupParams = @{ + OrganizationName = $OrganizationName + SecurityDescriptorId = $namespace.namespaceId + } + + # Get the ACL List and format the ACLS + Write-Verbose "[Get-AzDoGroupPermission] ACL Lookup Params: $($ACLLookupParams | Out-String)" + + $DifferenceACLs = Get-DevOpsACL @ACLLookupParams | ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName + $DifferenceACLs = $DifferenceACLs | Where-Object { + ($_.Token.Type -eq 'GroupPermission') -and + ($_.Token.GroupId -eq $group.originId) -and + ($_.Token.ProjectId -eq $project.id) + } + + # + # Iterate through each of the Permissions and append the permission identity if it contains 'Self' or 'This' + forEach ($Permission in $Permissions) + { + if ($Permission.Identity -in 'self', 'this') + { + $Permission.Identity = '[{0}]\{1}' -f $ProjectName, $GroupName + } + } + + Write-Verbose "[Get-AzDoGroupPermission] ACL List retrieved and formatted." + + # + # Convert the Permissions into an ACL Token + + $params = @{ + Permissions = $Permissions + SecurityNamespace = $SecurityNamespace + isInherited = $isInherited + OrganizationName = $OrganizationName + TokenName = '{0}\\{1}' -f $project.id, $group.id + } + + # Convert the Permissions to an ACL Token + $ReferenceACLs = ConvertTo-ACL @params | Where-Object { $_.token.Type -ne 'GroupUnknown' } + + # if the ACEs are empty, skip + if ($ReferenceACLs.aces.Count -eq 0) + { + Write-Verbose "[Get-AzDoGroupPermission] No ACEs found for the group." + return + } + + # Compare the Reference ACLs to the Difference ACLs + $compareResult = Test-ACLListforChanges -ReferenceACLs $ReferenceACLs -DifferenceACLs $DifferenceACLs + $getGroupResult.propertiesChanged = $compareResult.propertiesChanged + $getGroupResult.status = [DSCGetSummaryState]::"$($compareResult.status)" + $getGroupResult.reason = $compareResult.reason + + # Export the ACL List to a file + $getGroupResult.ReferenceACLs = $ReferenceACLs + $getGroupResult.DifferenceACLs = $DifferenceACLs + + # Write + Write-Verbose "[Get-AzDoGroupPermission] Result Status: $($getGroupResult.status)" + Write-Verbose "[Get-AzDoGroupPermission] Returning Group Result." + + # Return the Group Result + return $getGroupResult + +} + diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/New-AzDoGroupPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/New-AzDoGroupPermission.ps1 new file mode 100644 index 000000000..faab7cc8a --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/New-AzDoGroupPermission.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS +Creates a new Azure DevOps group permission. + +.DESCRIPTION +The New-AzDoGroupPermission function creates a new permission set for a specified Azure DevOps group. +It formats the group name, retrieves necessary security namespace and project information, +serializes the ACLs, and sets the permissions accordingly. + +.PARAMETER GroupName +Specifies the name of the group for which the permissions are being set. This parameter is mandatory. + +.PARAMETER isInherited +Indicates whether the permissions are inherited. This parameter is mandatory. + +.PARAMETER Permissions +Specifies a hashtable array of permissions to be applied. This parameter is optional. + +.PARAMETER LookupResult +Specifies a hashtable containing lookup results. This parameter is optional. + +.PARAMETER Ensure +Specifies the desired state of the permissions. This parameter is optional. + +.PARAMETER Force +Forces the command to run without asking for user confirmation. This parameter is optional. + +.EXAMPLE +New-AzDoGroupPermission -GroupName "ProjectName\GroupName" -isInherited $true -Permissions $permissions -LookupResult $lookupResult -Ensure Present -Force + +.NOTES +This function requires the Azure DevOps PowerShell module and appropriate permissions to set group permissions. +#> +Function New-AzDoGroupPermission +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$GroupName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[New-AzDoProjectGroupPermission] Started." + + # + # Format the Group Name + + # Split the Group Name + $split = $GroupName.Split('\').Split('/') + + # Test if the Group Name is valid + if ($split.Count -ne 2) + { + Write-Warning "[Get-AzDoProjectGroupPermission] Invalid Group Name: $GroupName" + return + } + + # Define the Project and Group Name + $ProjectName = $split[0] + $GroupName = $split[1] + + # + # Security Namespace ID + + $SecurityNamespace = Get-CacheItem -Key 'Identity' -Type 'SecurityNamespaces' + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + $Group = Get-CacheItem -Key $('[{0}]\{1}' -f $ProjectName, $GroupName) -Type 'LiveGroups' + + # + # Serialize the ACLs + + $serializeACLParams = @{ + ReferenceACLs = $LookupResult.propertiesChanged + DescriptorACLList = Get-CacheItem -Key $SecurityNamespace.namespaceId -Type 'LiveACLList' + DescriptorMatchToken = ($LocalizedDataAzSerializationPatten.GroupPermission -f $Project.id, $Group.id) + } + + $params = @{ + OrganizationName = $Global:DSCAZDO_OrganizationName + SecurityNamespaceID = $SecurityNamespace.namespaceId + SerializedACLs = ConvertTo-ACLHashtable @serializeACLParams + } + + # + # Set the Git Repository Permissions + + Set-AzDoPermission @params + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Remove-AzDoGroupPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Remove-AzDoGroupPermission.ps1 new file mode 100644 index 000000000..ada4eba6f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Remove-AzDoGroupPermission.ps1 @@ -0,0 +1,112 @@ +<# +.SYNOPSIS +Removes Azure DevOps group permissions for a specified group. + +.DESCRIPTION +The Remove-AzDoGroupPermission function removes permissions for a specified group in Azure DevOps. +It validates the group name, retrieves the necessary security namespace and project information, +and removes the Access Control Lists (ACLs) associated with the group if they exist. + +.PARAMETER GroupName +Specifies the name of the group whose permissions are to be removed. This parameter is mandatory. + +.PARAMETER isInherited +Indicates whether the permissions are inherited. This parameter is mandatory. + +.PARAMETER Permissions +Specifies a hashtable array of permissions to be removed. This parameter is optional. + +.PARAMETER LookupResult +Specifies a hashtable for lookup results. This parameter is optional. + +.PARAMETER Ensure +Specifies the desired state of the permissions. This parameter is optional. + +.PARAMETER Force +Forces the removal of permissions without prompting for confirmation. This parameter is optional. + +.EXAMPLE +Remove-AzDoGroupPermission -GroupName "ProjectName\GroupName" -isInherited $true + +This example removes the permissions for the specified group in the given project. + +.NOTES +The function uses cached items to retrieve security namespace, project, and repository information. +It filters the ACLs related to the Git repository and removes them if they exist. +#> +Function Remove-AzDoGroupPermission +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$GroupName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + + Write-Verbose "[Remove-AzDoGroupPermission] Started." + + # + # Format the Group Name + + # Split the Group Name + $split = $GroupName.Split('\').Split('/') + + # Test if the Group Name is valid + if ($split.Count -ne 2) + { + Write-Warning "[Get-AzDoProjectGroupPermission] Invalid Group Name: $GroupName" + return + } + + # Define the Project and Group Name + $ProjectName = $split[0] + $GroupName = $split[1] + + # + # Security Namespace ID + + $SecurityNamespace = Get-CacheItem -Key 'Identity' -Type 'SecurityNamespaces' + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + $Repository = Get-CacheItem -Key "$ProjectName\$RepositoryName" -Type 'LiveRepositories' + $DescriptorACLList = Get-CacheItem -Key $SecurityNamespace.namespaceId -Type 'LiveACLList' + + # + # Filter the ACLs that pertain to the Git Repository + + $searchString = 'repoV2/{0}/{1}' -f $Project.id, $Repository.id + + # Test if the Token exists + $Filtered = $DescriptorACLList | Where-Object { $_.token -eq $searchString } + + # If the ACLs are not null, remove them + if ($Filtered) + { + + $params = @{ + OrganizationName = $Global:DSCAZDO_OrganizationName + SecurityNamespaceID = $SecurityNamespace.namespaceId + TokenName = $searchString + } + + # Remove the ACLs + Remove-AzDoPermission @params + + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Set-AzDoGroupPermission.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Set-AzDoGroupPermission.ps1 new file mode 100644 index 000000000..8b4c779c4 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Set-AzDoGroupPermission.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS +Sets Azure DevOps group permissions. + +.DESCRIPTION +The Set-AzDoGroupPermission function sets permissions for a specified Azure DevOps group. +It formats the group name, retrieves necessary security namespace and project information, +serializes ACLs, and applies the permissions. + +.PARAMETER GroupName +The name of the group for which permissions are being set. This parameter is mandatory. + +.PARAMETER isInherited +A boolean value indicating whether the permissions are inherited. This parameter is mandatory. + +.PARAMETER Permissions +A hashtable array containing the permissions to be set. This parameter is optional. + +.PARAMETER LookupResult +A hashtable containing the lookup results. This parameter is optional. + +.PARAMETER Ensure +Specifies whether the permissions should be ensured. This parameter is optional. + +.PARAMETER Force +A switch parameter to force the operation. This parameter is optional. + +.EXAMPLE +Set-AzDoGroupPermission -GroupName "ProjectName\GroupName" -isInherited $true -Permissions $permissions -LookupResult $lookupResult -Ensure Present -Force + +.NOTES +This function relies on cached items for security namespace and project information. +#> + +Function Set-AzDoGroupPermission +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$GroupName, + + [Parameter(Mandatory = $true)] + [bool]$isInherited, + + [Parameter()] + [HashTable[]]$Permissions, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + Write-Verbose "[Set-AzDoGroupPermission] Started." + + # + # Format the Group Name + + # Split the Group Name + $split = $GroupName.Split('\').Split('/') + + # Test if the Group Name is valid + if ($split.Count -ne 2) + { + Write-Warning "[Get-AzDoProjectGroupPermission] Invalid Group Name: $GroupName" + return + } + + # Define the Project and Group Name + $ProjectName = $split[0] + $GroupName = $split[1] + + # + # Security Namespace ID + + $SecurityNamespace = Get-CacheItem -Key 'Identity' -Type 'SecurityNamespaces' + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + # + # Serialize the ACLs + + $serializeACLParams = @{ + ReferenceACLs = $LookupResult.propertiesChanged + DescriptorACLList = Get-CacheItem -Key $SecurityNamespace.namespaceId -Type 'LiveACLList' + DescriptorMatchToken = ($LocalizedDataAzSerializationPatten.GitRepository -f $Project.id) + } + + $params = @{ + OrganizationName = $Global:DSCAZDO_OrganizationName + SecurityNamespaceID = $SecurityNamespace.namespaceId + SerializedACLs = ConvertTo-ACLHashtable @serializeACLParams + } + + # + # Set the Git Repository Permissions + + Set-AzDoPermission @params + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Get-AzDoOrganizationGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Get-AzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..c4ea98412 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Get-AzDoOrganizationGroup.ps1 @@ -0,0 +1,231 @@ +<# +.SYNOPSIS +Retrieves an organization group from Azure DevOps. + +.DESCRIPTION +The Get-AzDoOrganizationGroup function retrieves an organization group from Azure DevOps based on the provided parameters. + +.PARAMETER ApiUri +The URI of the Azure DevOps API. This parameter is validated using the Test-AzDevOpsApiUri function. + +.PARAMETER Pat +The Personal Access Token (PAT) used for authentication. This parameter is validated using the Test-AzDevOpsPat function. + +.PARAMETER GroupName +The name of the organization group to retrieve. + +.OUTPUTS +[System.Management.Automation.PSObject[]] +The retrieved organization group. + +.EXAMPLE +Get-AzDoOrganizationGroup -ApiUri 'https://dev.azure.com/contoso' -Pat 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx' -GroupName 'Developers' +Retrieves the organization group named 'Developers' from the Azure DevOps instance at 'https://dev.azure.com/contoso' using the provided PAT. + +#> + +Function Get-AzDoOrganizationGroup +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure + ) + + # Logging + Write-Verbose "[Get-AzDoOrganizationGroup] Retriving the GroupName from the Live and Local Cache." + + # Format the Key According to the Principal Name + $Key = Format-AzDoGroup -Prefix "[$Global:DSCAZDO_OrganizationName]" -GroupName $GroupName + + # Check the cache for the group + $livegroup = Get-CacheItem -Key $Key -Type 'LiveGroups' + + # + # Check if the group is in the cache + $localgroup = Get-CacheItem -Key $Key -Type 'Group' + + Write-Verbose "[Get-AzDoOrganizationGroup] GroupName: '$GroupName'" + + # Construct a hashtable detailing the group + $getGroupResult = @{ + #Reasons = $() + Ensure = [Ensure]::Absent + localCache = $localgroup + liveCache = $livegroup + propertiesChanged = @() + status = $null + } + + Write-Verbose "[Get-AzDoOrganizationGroup] Testing LocalCache, LiveCache and Parameters." + + # + # If the localgroup and lifegroup are present, compare the properties as well as the originId + if (($null -ne $livegroup.originId) -and ($null -ne $localgroup.originId)) + { + Write-Verbose "[Get-AzDoOrganizationGroup] Testing LocalCache, LiveCache and Parameters." + + # + # Check if the originId is the same. If so, the group is unchanged. If not, the group has been renamed. + + if ($livegroup.originId -ne $localgroup.originId) + { + # The group has been renamed or deleted and recreated. + + # Perform a lookup in the live cache to see if the group has been deleted and recreated. + $renamedGroup = $livegroup | Find-CacheItem { $_.originId -eq $livegroup.originId } + + # If renamed group is not null, the group has been renamed. + if ($null -ne $renamedGroup) + { + # Add the renamed group to result + $getGroupResult.renamedGroup = $renamedGroup + # The group has been renamed. + $getGroupResult.status = [DSCGetSummaryState]::Renamed + } + else + { + # The group has been deleted and recreated. Treat the new group as the live group. + + # Remove the old group from the local cache + Remove-CacheItem -Key $Key -Type 'Group' + # Add the new group to the local cache + Add-CacheItem -Key $Key -Value $livegroup -Type 'Group' + + # Compare the properties of the live group with the parameters + if ($livegroup.description -ne $groupDescription) + { + $getGroupResult.propertiesChanged += 'description' + } + + if ($livegroup.name -ne $localgroup.name){ + $getGroupResult.propertiesChanged += 'displayName' + } + + # If the properties are the same, the group is unchanged. If not, the group has been changed. + if ($getGroupResult.propertiesChanged.count -ne 0) + { + # Update the Result + $getGroupResult.status = [DSCGetSummaryState]::Changed + } + else + { + # Update the Result + $getGroupResult.status = [DSCGetSummaryState]::Unchanged + } + + } + return $getGroupResult + } + + # The Group hasn't been renamed. Test the properties to make sure they are the same as the parameters. + + # Compare the properties of the live group with the parameters + if ($livegroup.description -ne $groupDescription) + { + $getGroupResult.propertiesChanged += 'Description' + } + if ($livegroup.name -ne $localgroup.name) + { + $getGroupResult.propertiesChanged += 'Name' + } + + # If the properties are the same, the group is unchanged. If not, the group has been changed. + $getGroupResult.status = $( + if ($getGroupResult.propertiesChanged.count -ne 0) + { + [DSCGetSummaryState]::Changed + } + else + { + [DSCGetSummaryState]::Unchanged + } + ) + + if ($getGroupResult.status -ne [DSCGetSummaryState]::Changed) + { + $getGroupResult.Ensure = [Ensure]::Present + } + + # Return the group from the cache + return $getGroupResult + + } + + # If the livegroup is not present and the localgroup is present, the group is missing and recreate it. + if (($null -eq $livegroup) -and ($null -ne $localgroup)) + { + $getGroupResult.status = [DSCGetSummaryState]::NotFound + $getGroupResult.propertiesChanged = @('description', 'displayName') + # Add the reason + return $getGroupResult + } + + <# + If the localgroup is not present and the livegroup is present, the group is not found. Check the properties are the same as the parameters. + If the properties are the same, the group is unchanged. If not, the group has been deleted and then recreated and the new group will become authoritative. + #> + + if (($null -eq $localgroup) -and ($null -ne $livegroup)) + { + + # Validate that the live properties are the same as the parameters + if ($livegroup.description -ne $GroupDescription ) + { + $getGroupResult.propertiesChanged += 'description' + } + if ($livegroup.displayName -ne $GroupName ) + { + $getGroupResult.propertiesChanged += 'displayName' + } + + # If the properties are the same, the group is unchanged. If not, the group has been changed. + $getGroupResult.status = $( + if ($getGroupResult.propertiesChanged.count -ne 0) + { + [DSCGetSummaryState]::Changed + } + else + { + [DSCGetSummaryState]::Unchanged + } + ) + + if ($getGroupResult.status -ne [DSCGetSummaryState]::Unchanged) + { + # Set the Ensure to Present + $getGroupResult.Ensure = [Ensure]::Present + } + else + { + # Add the unchanged group to the local cache + Add-CacheItem -Key $Key -Value $livegroup -Type 'Group' + } + + # Return the group from the cache + return $getGroupResult + + } + + # If the livegroup and localgroup are not present, the group is missing and recreate it. + if (($null -eq $livegroup) -and ($null -eq $localgroup)) + { + $getGroupResult.status = [DSCGetSummaryState]::NotFound + $getGroupResult.propertiesChanged = @('description', 'displayName') + return $getGroupResult + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/New-AzDoOrganizationGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/New-AzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..f65ca9389 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/New-AzDoOrganizationGroup.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS +Creates a new Azure DevOps organization group. + +.DESCRIPTION +The New-AzDoOrganizationGroup function creates a new group in an Azure DevOps organization. +It accepts parameters for the group name, description, lookup result, ensure, and force. +The function logs verbose messages during the creation process and updates the cache with the new group information. + +.PARAMETER GroupName +Specifies the name of the group to be created. This parameter is mandatory. + +.PARAMETER GroupDescription +Specifies the description of the group to be created. This parameter is optional. + +.PARAMETER LookupResult +Specifies a hashtable for lookup results. This parameter is optional. + +.PARAMETER Ensure +Specifies the desired state of the group. This parameter is optional. + +.PARAMETER Force +Forces the creation of the group without confirmation. This parameter is optional. + +.EXAMPLE +PS C:\> New-AzDoOrganizationGroup -GroupName "Developers" -GroupDescription "Development Team" + +This command creates a new Azure DevOps group named "Developers" with the description "Development Team". + +#> +Function New-AzDoOrganizationGroup +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Define parameters for creating a new DevOps group + $params = @{ + GroupName = $GroupName + GroupDescription = $GroupDescription + ApiUri = 'https://vssps.dev.azure.com/{0}' -f $Global:DSCAZDO_OrganizationName + } + + # Write verbose log with the parameters used for creating the group + Write-Verbose "[New-AzDoOrganizationGroup] Creating a new DevOps group with GroupName: '$($params.GroupName)', GroupDescription: '$($params.GroupDescription)' and ApiUri: '$($params.ApiUri)'" + + # Create a new group + $group = New-DevOpsGroup @params + + # Update the cache with the new group + Refresh-CacheIdentity -Identity $group -Key $group.principalName -CacheType 'LiveGroups' + + # Add the group to the Group cache and write to verbose log + Add-CacheItem -Key $group.principalName -Value $group -Type 'Group' + Write-Verbose "[New-AzDoOrganizationGroup] Added new group to Group cache with key: '$($group.principalName)'" + + # Update the global AzDoGroup object and write to verbose log + Set-CacheObject -Content $Global:AzDoGroup -CacheType 'Group' + Write-Verbose "[New-AzDoOrganizationGroup] Updated global AzDoGroup cache object." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Remove-AzDoOrganizationGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Remove-AzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..19ef7b6a0 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Remove-AzDoOrganizationGroup.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS +Removes an Azure DevOps organization group. + +.DESCRIPTION +The Remove-AzDoOrganizationGroup function removes a specified Azure DevOps organization group. +It uses the provided group name, description, and lookup result to identify and remove the group +from the Azure DevOps API and local cache. + +.PARAMETER GroupName +The name of the group to be removed. This parameter is mandatory. + +.PARAMETER GroupDescription +The description of the group to be removed. This parameter is optional. + +.PARAMETER LookupResult +A hashtable containing the lookup result for the group. This parameter is optional. + +.PARAMETER Ensure +Specifies whether the group should be present or absent. This parameter is optional. + +.PARAMETER Force +A switch parameter to force the removal of the group without confirmation. This parameter is optional. + +.EXAMPLE +Remove-AzDoOrganizationGroup -GroupName "Developers" -Force + +This example removes the "Developers" group from the Azure DevOps organization without confirmation. + +.NOTES +This function relies on the global variables $Global:DSCAZDO_OrganizationName, $Global:AZDOLiveGroups, +and $Global:AzDoGroup to interact with the Azure DevOps API and manage cache objects. +#> +Function Remove-AzDoOrganizationGroup +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # If no cache items exist, return. + if (($null -eq $LookupResult.liveCache) -and ($null -eq $LookupResult.localCache)) + { + return + } + + $params = @{ + GroupDescriptor = $LookupResult.liveCache.Descriptor + ApiUri = 'https://vssps.dev.azure.com/{0}' -f $Global:DSCAZDO_OrganizationName + } + + $cacheItem = @{ + Key = $LookupResult.liveCache.principalName + } + + # If the group is not found, return + if (($null -ne $LookupResult.localCache) -and ($null -eq $LookupResult.liveCache)) + { + $cacheItem.Key = $LookupResult.localCache.principalName + $params.GroupDescriptor = $LookupResult.localCache.Descriptor + } + + # + # Remove the group from the API + $null = Remove-DevOpsGroup @params + + # + # Remove the group from the API + + Remove-CacheItem @cacheItem -Type 'LiveGroups' + Set-CacheObject -Content $Global:AZDOLiveGroups -CacheType 'LiveGroups' + + Remove-CacheItem @cacheItem -Type 'Group' + Set-CacheObject -Content $Global:AzDoGroup -CacheType 'Group' + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Set-AzDoOrganizationGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Set-AzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..5e8f0dfa5 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Set-AzDoOrganizationGroup.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS +Sets or updates an Azure DevOps organization group. + +.DESCRIPTION +The Set-AzDoOrganizationGroup function sets or updates an Azure DevOps organization group based on the provided parameters. +It handles renaming, updating group details, and managing cache updates. + +.PARAMETER GroupName +Specifies the name of the group to be set or updated. This parameter is mandatory. + +.PARAMETER GroupDescription +Specifies the description of the group to be set or updated. This parameter is optional. + +.PARAMETER LookupResult +A hashtable containing the lookup result, which includes the status and cache information of the group. This parameter is optional. + +.PARAMETER Ensure +Specifies the desired state of the group. This parameter is optional. + +.PARAMETER Force +A switch parameter that forces the operation to proceed without confirmation. This parameter is optional. + +.EXAMPLE +Set-AzDoOrganizationGroup -GroupName "Developers" -GroupDescription "Development Team" -LookupResult $lookupResult + +This example sets or updates the "Developers" group with the description "Development Team" using the provided lookup result. + +.NOTES +If the group has been renamed, a warning is written and the function returns without making any changes. +The function updates the group using the Azure DevOps API and refreshes the cache with the new group information. +#> + +Function Set-AzDoOrganizationGroup +{ + param( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # + # Depending on the type of lookup status, the group has been renamed the group has been deleted and recreated. + if ($LookupResult.Status -eq [DSCGetSummaryState]::Renamed) + { + # For the time being write a warning and return + Write-Warning "[Set-AzDoOrganizationGroup] The group has been renamed. The group will not be set." + return + } + + # + # Update the group + $params = @{ + ApiUri = 'https://vssps.dev.azure.com/{0}' -f $Global:DSCAZDO_OrganizationName + GroupName = $GroupName + GroupDescription = $GroupDescription + GroupDescriptor = $LookupResult.liveCache.descriptor + } + + try + { + # Set the group from the API + $group = Set-DevOpsGroup @params + } + catch + { + throw $_ + } + + # + # Update the cache with the new group + Refresh-CacheIdentity -Identity $group -Key $group.principalName -CacheType 'LiveGroups' + + # + # Secondarily Replace the local cache with the new group + + if ($null -ne $LookupResult.localCache) + { + Remove-CacheItem -Key $LookupResult.localCache.principalName -Type 'Group' + } + + Add-CacheItem -Key $group.principalName -Value $group -Type 'Group' + Set-CacheObject -Content $Global:AzDoGroup -CacheType 'Group' + + # + # Return the group from the cache + return $group + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Test-AzDoOrganizationGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Test-AzDoOrganizationGroup.ps1 new file mode 100644 index 000000000..25d41b5d9 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Test-AzDoOrganizationGroup.ps1 @@ -0,0 +1,108 @@ +<# +.SYNOPSIS + Tests if an organization group exists in Azure DevOps. + +.DESCRIPTION + The Test-AzDoOrganizationGroup function checks if a specified organization group exists in Azure DevOps. + It uses a personal access token (PAT) and the Azure DevOps API to perform the check. + +.PARAMETER GroupName + Specifies the name of the organization group to test. + +.PARAMETER Pat + Specifies the personal access token (PAT) to authenticate with Azure DevOps. + The PAT is validated using the Test-AzDevOpsPat function. + +.PARAMETER ApiUri + Specifies the URI of the Azure DevOps API to connect to. + The URI is validated using the Test-AzDevOpsApiUri function. + +.OUTPUTS + System.Boolean + Returns $true if the organization group exists, otherwise returns $false. + +.EXAMPLE + Test-AzDoOrganizationGroup -GroupName 'MyGroup' -Pat '********' -ApiUri 'https://dev.azure.com/myorg' + + Description + ----------- + Tests if the organization group named 'MyGroup' exists in the Azure DevOps organization 'myorg' + using the specified personal access token and API URI. + +#> +Function Test-AzDoOrganizationGroup +{ + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $GroupName, + + [Parameter()] + [string] + $GroupDescription=$null, + + [Parameter()] + [Alias('Name')] + [hashtable]$GetResult + ) + + # Firstly we need to compare to see if the group names are the same. If so we can return $false. + if ($GetResult.Status -eq [DSCGetSummaryState]::Unchanged ) + { + + $result = $true + + if ($GroupDescription -eq $GetResult.Current.description) + { + $result = $false + } + + return $true + } + + # If the status has been flagged as 'Renamed', returned $true. This means that the originId has changed. + if ($GetResult.Status -eq [DSCGetSummaryState]::Renamed) + { + return $false + } + + # If the status has been flagged as 'Missing', returned $true. This means that the group is missing from the live cache. + if ($GetResult.Status -eq [DSCGetSummaryState]::Changed) + { + # If the group is present in the live cache and the local cache. This means that the originId has changed. This needs to be updated. + if (($null -ne $GetResult.Current) -and ($null -ne $GetResult.Cache)) + { + return $true + } + + # If the group is present in the live cache but not in the local cache. Flag as Changed. + if ($GetResult.Current -and -not($GetResult.Cache)) + { + return $true + } + + # If the group is not present in the live cache but is in the local cache. Flag as Changed. + if (-not($GetResult.Current) -and $GetResult.Cache) + { + return $true + } + + } + + # Format the Key According to the Principal Name + $Key = Format-AzDoGroup -Prefix "[$Global:DSCAZDO_OrganizationName]" -GroupName $GroupName + + # + # Check the cache for the group + $group = Get-CacheItem -Key $Key -Type 'LiveGroups' + if (-not($group)) + { + $false + } + else + { + $true + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Get-AzDoProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Get-AzDoProject.ps1 new file mode 100644 index 000000000..a280ca3cc --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Get-AzDoProject.ps1 @@ -0,0 +1,160 @@ +<# +.SYNOPSIS + Retrieves information about an Azure DevOps project. + +.DESCRIPTION + The Get-AzDoProject function retrieves details about an Azure DevOps project, including its name, description, source control type, process template, and visibility. It performs lookups to check if the project and process template exist and returns the project's status and any properties that have changed. + +.PARAMETER ProjectName + The name of the Azure DevOps project. This parameter is validated using the Test-AzDevOpsProjectName function. + +.PARAMETER ProjectDescription + The description of the Azure DevOps project. Defaults to an empty string if not specified. + +.PARAMETER SourceControlType + The source control type of the Azure DevOps project. Valid values are 'Git' and 'Tfvc'. Defaults to 'Git'. + +.PARAMETER ProcessTemplate + The process template used by the Azure DevOps project. Valid values are 'Agile', 'Scrum', 'CMMI', and 'Basic'. Defaults to 'Agile'. + +.PARAMETER Visibility + The visibility of the Azure DevOps project. Valid values are 'Public' and 'Private'. Defaults to 'Private'. + +.PARAMETER LookupResult + A hashtable to store the lookup result. + +.PARAMETER Ensure + Specifies the desired state of the project. + +.OUTPUTS + [System.Management.Automation.PSObject[]] + Returns a hashtable containing the project's details and status. + +.EXAMPLE + Get-AzDoProject -ProjectName "MyProject" -ProjectDescription "Sample project" -SourceControlType "Git" -ProcessTemplate "Agile" -Visibility "Private" + + Retrieves information about the Azure DevOps project named "MyProject" with the specified parameters. + +.NOTES + This function relies on global variables and other functions such as Get-CacheItem and Test-AzDevOpsProjectName to perform lookups and validations. +#> +function Get-AzDoProject +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param ( + [Parameter()] + [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid -AllowWildcard })] + [Alias('Name')] + [System.String] $ProjectName, + + [Parameter()] + [Alias('Description')] + [System.String] $ProjectDescription = '', + + [Parameter()] + [ValidateSet('Git', 'Tfvc')] + [System.String] $SourceControlType = 'Git', + + [Parameter()] + [ValidateSet('Agile', 'Scrum', 'CMMI', 'Basic')] + [System.String] $ProcessTemplate = 'Agile', + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String] $Visibility = 'Private', + + [Parameter()] + [HashTable] $LookupResult, + + [Parameter()] + [Ensure] $Ensure + ) + + Write-Verbose "[Get-AzDoProject] Started." + + # Set the organization name + $OrganizationName = $Global:DSCAZDO_OrganizationName + Write-Verbose "[Get-AzDoProject] Organization Name: $OrganizationName" + + # Construct a hashtable detailing the group + $result = @{ + Ensure = [Ensure]::Absent + ProjectName = $ProjectName + ProjectDescription = $ProjectDescription + SourceControlType = $SourceControlType + ProcessTemplate = $ProcessTemplate + Visibility = $Visibility + propertiesChanged = @() + status = $null + } + + Write-Verbose "[Get-AzDoProject] Initial result hashtable constructed." + + # Perform a lookup to see if the project exists in Azure DevOps + $project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + # Set the project description to be a string if it is not already. + Write-Verbose "[Get-AzDoProject] Project lookup result: $project" + + $processTemplateObj = Get-CacheItem -Key $ProcessTemplate -Type 'LiveProcesses' + Write-Verbose "[Get-AzDoProject] Process template lookup result: $processTemplateObj" + + # Test if the project exists. If the project does not exist, return NotFound + if (($null -eq $project) -and ($null -ne $ProjectName)) + { + $result.Status = [DSCGetSummaryState]::NotFound + Write-Verbose "[Get-AzDoProject] Project not found." + return $result + } + + # Test if the process template exists. If the process template does not exist, throw an error. + if ($null -eq $processTemplateObj) + { + throw "[Get-AzDoProject] Process template '$processTemplateObj' not found." + } + + Write-Verbose "[Get-AzDoProject] Testing source control type." + + # Test if the project is using the same source control type. If the source control type is different, return a conflict. + if ($SourceControlType -ne $project.SourceControlType) + { + Write-Warning "[Get-AzDoProject] Source control type is different. Current: $($project.SourceControlType), Desired: $SourceControlType" + Write-Warning "[Get-AzDoProject] Source control type cannot be changed. Please delete the project and recreate it." + } + + # If the project description property does not exist. Create it and set it to an empty string. + if ($null -eq $project.description) + { + $project | Add-Member -MemberType NoteProperty -Name description -Value '' + Write-Verbose "[Get-AzDoProject] Project description was null, set to empty string." + } + + # Test if the project description is the same. If the description is different, return a conflict. + if ($ProjectDescription.Trim() -ne $project.description.Trim()) + { + $result.Status = [DSCGetSummaryState]::Changed + $result.propertiesChanged += 'Description' + Write-Verbose "[Get-AzDoProject] Project description has changed." + } + + # Test if the project visibility is the same. If the visibility is different, return a conflict. + if ($Visibility -ne $project.Visibility) + { + $result.Status = [DSCGetSummaryState]::Changed + $result.propertiesChanged += 'Visibility' + Write-Verbose "[Get-AzDoProject] Project visibility has changed." + } + + # Test if the properties have changed. If the properties haven't changed, return Unchanged. + if ($result.propertiesChanged.Count -eq 0) + { + $result.Status = [DSCGetSummaryState]::Unchanged + Write-Verbose "[Get-AzDoProject] Project properties have not changed." + } + + # Return the group from the cache + Write-Verbose "[Get-AzDoProject] Returning final result." + + return $result + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/New-AzDoProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/New-AzDoProject.ps1 new file mode 100644 index 000000000..6e5676dc2 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/New-AzDoProject.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS +Creates a new Azure DevOps project. + +.DESCRIPTION +The New-AzDoProject function creates a new project in Azure DevOps with the specified parameters. +It supports setting the project name, description, source control type, process template, and visibility. +The function also ensures the project is created by waiting for the project creation job to complete and refreshes the cache once the project is created. + +.PARAMETER ProjectName +Specifies the name of the Azure DevOps project. The name is validated using the Test-AzDevOpsProjectName function. + +.PARAMETER ProjectDescription +Specifies the description of the Azure DevOps project. + +.PARAMETER SourceControlType +Specifies the type of source control for the project. Valid values are 'Git' and 'Tfvc'. The default value is 'Git'. + +.PARAMETER ProcessTemplate +Specifies the process template for the project. Valid values are 'Agile', 'Scrum', 'CMMI', and 'Basic'. The default value is 'Agile'. + +.PARAMETER Visibility +Specifies the visibility of the project. Valid values are 'Public' and 'Private'. The default value is 'Private'. + +.PARAMETER LookupResult +Specifies a hashtable for lookup results. + +.PARAMETER Ensure +Specifies the desired state of the project. + +.PARAMETER Force +Forces the creation of the project without prompting for confirmation. + +.EXAMPLE +PS C:\> New-AzDoProject -ProjectName "MyProject" -ProjectDescription "This is a sample project" -SourceControlType "Git" -ProcessTemplate "Agile" -Visibility "Private" + +Creates a new Azure DevOps project named "MyProject" with the specified description, source control type, process template, and visibility. + +#> +function New-AzDoProject +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid -AllowWildcard })] + [Alias('Name')] + [System.String] + $ProjectName, + + [Parameter()] + [Alias('Description')] + [System.String] + $ProjectDescription, + + [Parameter()] + [ValidateSet('Git','Tfvc')] + [System.String] + $SourceControlType = 'Git', + + [Parameter()] + [ValidateSet('Agile', 'Scrum', 'CMMI', 'Basic')] + [System.String]$ProcessTemplate = 'Agile', + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String]$Visibility = 'Private', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Set the organization name + $OrganizationName = $Global:DSCAZDO_OrganizationName + + # + # Perform a lookup to see if the group exists in Azure DevOps + $processTemplateObj = Get-CacheItem -Key $ProcessTemplate -Type 'LiveProcesses' + + # + # Construct the parameters for the API call + $parameters = @{ + organization = $OrganizationName + projectName = $ProjectName + description = $ProjectDescription + sourceControlType = $SourceControlType + processTemplateId = $processTemplateObj.id + visibility = $Visibility + } + + # + # Create the project + + $projectJob = New-DevOpsProject @parameters + + # + # Wait for the project to be created + + Wait-DevOpsProject -ProjectURL $projectJob.url -OrganizationName $OrganizationName + + # + # Once the project has been created, refresh the entire cache. + + Refresh-AzDoCache -OrganizationName $OrganizationName + +} + + diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Remove-AzDoProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Remove-AzDoProject.ps1 new file mode 100644 index 000000000..49b30462f --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Remove-AzDoProject.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS +Removes an Azure DevOps project. + +.DESCRIPTION +The Remove-AzDoProject function removes a specified project from Azure DevOps. It performs a lookup to check if the project exists in the cache, removes it from Azure DevOps, and updates the local cache accordingly. + +.PARAMETER ProjectName +Specifies the name of the Azure DevOps project to be removed. This parameter is validated using the Test-AzDevOpsProjectName function. + +.PARAMETER ProjectDescription +Specifies the description of the Azure DevOps project. + +.PARAMETER SourceControlType +Specifies the type of source control for the project. Valid values are 'Git' and 'Tfvc'. The default value is 'Git'. + +.PARAMETER ProcessTemplate +Specifies the process template for the project. Valid values are 'Agile', 'Scrum', 'CMMI', and 'Basic'. The default value is 'Agile'. + +.PARAMETER Visibility +Specifies the visibility of the project. Valid values are 'Public' and 'Private'. The default value is 'Private'. + +.PARAMETER LookupResult +Specifies a hashtable to store the lookup result. + +.PARAMETER Ensure +Specifies the desired state of the project. + +.PARAMETER Force +Forces the removal of the project without prompting for confirmation. + +.EXAMPLE +Remove-AzDoProject -ProjectName "MyProject" -Force + +This command removes the Azure DevOps project named "MyProject" without prompting for confirmation. + +.NOTES +The function uses global variable $Global:DSCAZDO_OrganizationName to get the organization name. +#> +function Remove-AzDoProject +{ + [CmdletBinding()] + param ( + [Parameter()] + [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid -AllowWildcard })] + [Alias('Name')] + [System.String] $ProjectName, + + [Parameter()] + [Alias('Description')] + [System.String] $ProjectDescription, + + [Parameter()] + [ValidateSet('Git', 'Tfvc')] + [System.String] $SourceControlType = 'Git', + + [Parameter()] + [ValidateSet('Agile', 'Scrum', 'CMMI', 'Basic')] + [System.String] $ProcessTemplate = 'Agile', + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String] $Visibility = 'Private', + + [Parameter()] + [HashTable] $LookupResult, + + [Parameter()] + [Ensure] $Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] $Force + ) + + # Set the organization name + $OrganizationName = $Global:DSCAZDO_OrganizationName + Write-Verbose "[Remove-AzDoProject] Using organization name: $OrganizationName" + + # Perform a lookup to see if the group exists in Azure DevOps + Write-Verbose "[Remove-AzDoProject] Looking up project: $ProjectName" + $project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + if ($null -eq $project) + { + Write-Verbose "[Remove-AzDoProject] Project $ProjectName not found in cache." + return + } + + Write-Verbose "[Remove-AzDoProject] Found project $ProjectName with ID: $($project.id)" + + # Remove the project + Write-Verbose "[Remove-AzDoProject] Removing project $ProjectName from Azure DevOps" + Remove-DevOpsProject -Organization $OrganizationName -ProjectId $project.id + + # Remove the project from the cache and export the cache + Write-Verbose "[Remove-AzDoProject] Removing project $ProjectName from local cache" + Remove-CacheItem -Key $ProjectName -Type 'LiveProjects' + + Write-Verbose "[Remove-AzDoProject] Exporting updated cache object for LiveProjects" + Export-CacheObject -CacheType 'LiveProjects' -Content $AzDoLiveProjects +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Set-AzDoProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Set-AzDoProject.ps1 new file mode 100644 index 000000000..bad8b7615 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Set-AzDoProject.ps1 @@ -0,0 +1,110 @@ +<# +.SYNOPSIS +Updates an existing Azure DevOps project with the specified parameters. + +.DESCRIPTION +The Set-AzDoProject function updates an existing Azure DevOps project with the provided project name, description, source control type, process template, and visibility. It performs a lookup to see if the project exists in Azure DevOps, constructs the parameters for the API call, updates the project, waits for the update to complete, and refreshes the cache. + +.PARAMETER ProjectName +Specifies the name of the Azure DevOps project to update. This parameter is validated using the Test-AzDevOpsProjectName function. + +.PARAMETER ProjectDescription +Specifies the description of the Azure DevOps project. + +.PARAMETER SourceControlType +Specifies the source control type for the project. Valid values are 'Git' and 'Tfvc'. The default value is 'Git'. + +.PARAMETER ProcessTemplate +Specifies the process template for the project. Valid values are 'Agile', 'Scrum', 'CMMI', and 'Basic'. The default value is 'Agile'. + +.PARAMETER Visibility +Specifies the visibility of the project. Valid values are 'Public' and 'Private'. The default value is 'Private'. + +.PARAMETER LookupResult +Specifies a hashtable to store the lookup result. + +.PARAMETER Ensure +Specifies whether to ensure the project exists or not. + +.PARAMETER Force +Specifies whether to force the update of the project. + +.EXAMPLE +Set-AzDoProject -ProjectName "MyProject" -ProjectDescription "This is a sample project" -SourceControlType "Git" -ProcessTemplate "Agile" -Visibility "Private" + +This example updates the Azure DevOps project named "MyProject" with the specified description, source control type, process template, and visibility. + +#> +function Set-AzDoProject +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid -AllowWildcard })] + [Alias('Name')] + [System.String] + $ProjectName, + + [Parameter()] + [Alias('Description')] + [System.String] + $ProjectDescription, + + [Parameter()] + [ValidateSet('Git','Tfvc')] + [System.String] + $SourceControlType = 'Git', + + [Parameter()] + [ValidateSet('Agile', 'Scrum', 'CMMI', 'Basic')] + [System.String]$ProcessTemplate = 'Agile', + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String]$Visibility = 'Private', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + $OrganizationName = $Global:DSCAZDO_OrganizationName + + # + # Perform a lookup to see if the group exists in Azure DevOps + $project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + $processTemplateObj = Get-CacheItem -Key $ProcessTemplate -Type 'LiveProcesses' + + # + # Construct the parameters for the API call + $parameters = @{ + organization = $OrganizationName + projectId = $project.id + description = $ProjectDescription + processTemplateId = $processTemplateObj.id + visibility = $Visibility + } + + # + # Update the project + + $projectJob = Update-DevOpsProject @parameters + + # + # Wait for the project to be updated + + Wait-DevOpsProject -ProjectURL $projectJob.url -OrganizationName $OrganizationName + + # + # Once the project has been created, refresh the entire cache. + + Refresh-AzDoCache -OrganizationName $OrganizationName + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Test-AzDoProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Test-AzDoProject.ps1 new file mode 100644 index 000000000..30febf93a --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Test-AzDoProject.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + Tests the existence and properties of an Azure DevOps project. + +.DESCRIPTION + The Test-AzDoProject function checks if an Azure DevOps project exists and validates its properties such as name, description, source control type, process template, and visibility. + +.PARAMETER ProjectName + The name of the Azure DevOps project. This parameter is validated using the Test-AzDevOpsProjectName function. + +.PARAMETER ProjectDescription + The description of the Azure DevOps project. + +.PARAMETER SourceControlType + The type of source control used by the project. Valid values are 'Git' and 'Tfvc'. The default value is 'Git'. + +.PARAMETER ProcessTemplate + The process template used by the project. Valid values are 'Agile', 'Scrum', 'CMMI', and 'Basic'. The default value is 'Agile'. + +.PARAMETER Visibility + The visibility of the project. Valid values are 'Public' and 'Private'. The default value is 'Private'. + +.PARAMETER LookupResult + A PSCustomObject that contains the lookup result for the project. + +.PARAMETER Ensure + Specifies whether the project should exist or not. + +.PARAMETER Force + A switch parameter to force the operation. + +.EXAMPLE + Test-AzDoProject -ProjectName "MyProject" -ProjectDescription "This is a sample project" -SourceControlType "Git" -ProcessTemplate "Agile" -Visibility "Private" + +.NOTES + This function is a placeholder and should not be triggered. +#> +function Test-AzDoProject +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid -AllowWildcard })] + [Alias('Name')] + [System.String] + $ProjectName, + + [Parameter()] + [Alias('Description')] + [System.String] + $ProjectDescription, + + [Parameter()] + [ValidateSet('Git','Tfvc')] + [System.String] + $SourceControlType = 'Git', + + [Parameter()] + [ValidateSet('Agile', 'Scrum', 'CMMI', 'Basic')] + [System.String]$ProcessTemplate = 'Agile', + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String]$Visibility = 'Private', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Should not be triggered. This is a placeholder for the test function. + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Get-AzDoProjectGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Get-AzDoProjectGroup.ps1 new file mode 100644 index 000000000..e91721064 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Get-AzDoProjectGroup.ps1 @@ -0,0 +1,219 @@ +<# +.SYNOPSIS +Retrieves an organization group from Azure DevOps. + +.DESCRIPTION +The Get-AzDoProjectGroup function retrieves an organization group from Azure DevOps based on the provided parameters. + +.PARAMETER ApiUri +The URI of the Azure DevOps API. This parameter is validated using the Test-AzDevOpsApiUri function. + +.PARAMETER Pat +The Personal Access Token (PAT) used for authentication. This parameter is validated using the Test-AzDevOpsPat function. + +.PARAMETER GroupName +The name of the organization group to retrieve. + +.OUTPUTS +[System.Management.Automation.PSObject[]] +The retrieved organization group. + +.EXAMPLE +Get-AzDoProjectGroup -ApiUri 'https://dev.azure.com/contoso' -Pat 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx' -GroupName 'Developers' +Retrieves the organization group named 'Developers' from the Azure DevOps instance at 'https://dev.azure.com/contoso' using the provided PAT. + +#> + +Function Get-AzDoProjectGroup +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Project')] + [System.String]$ProjectName, + + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure + ) + + # Logging + Write-Verbose "[Get-AzDoProjectGroup] Retriving the GroupName from the Live and Local Cache." + + # + # Format the Key According to the Principal Name + $Key = Format-AzDoGroup -Prefix "[$ProjectName]" -GroupName $GroupName + + # + # Check the cache for the group + $livegroup = Get-CacheItem -Key $Key -Type 'LiveGroups' + + # + # Check if the group is in the cache + $localgroup = Get-CacheItem -Key $Key -Type 'Group' + + # + # Retrive the Project + $project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + Write-Verbose "[Get-AzDoProjectGroup] GroupName: '$GroupName'" + + # + # Construct a hashtable detailing the group + $getGroupResult = @{ + #Reasons = $() + Ensure = [Ensure]::Absent + localCache = $localgroup + liveCache = $livegroup + propertiesChanged = @() + status = $null + project = $project + } + + Write-Verbose "[Get-AzDoProjectGroup] Testing LocalCache, LiveCache and Parameters." + + # If the localgroup and lifegroup are present, compare the properties as well as the originId + if (($null -ne $livegroup.originId) -and ($null -ne $localgroup.originId)) + { + + Write-Verbose "[Get-AzDoProjectGroup] Testing LocalCache, LiveCache and Parameters." + + # Check if the originId is the same. If so, the group is unchanged. If not, the group has been renamed. + if ($livegroup.originId -ne $localgroup.originId) + { + # The group has been renamed or deleted and recreated. + + # Perform a lookup in the live cache to see if the group has been deleted and recreated. + $renamedGroup = $livegroup | Find-CacheItem -Filter { $_.originId -eq $livegroup.originId } + + # If renamed group is not null, the group has been renamed. + if ($null -ne $renamedGroup) + { + # Add the renamed group to result + $getGroupResult.renamedGroup = $renamedGroup + # The group has been renamed. + $getGroupResult.status = [DSCGetSummaryState]::Renamed + + } + else + { + # The group has been deleted and recreated. Treat the new group as the live group. + + # Remove the old group from the local cache + Remove-CacheItem -Key $Key -Type 'Group' + # Add the new group to the local cache + Add-CacheItem -Key $Key -Value $livegroup -Type 'Group' + + # Compare the properties of the live group with the parameters + if ($livegroup.description -ne $groupDescription) + { + $getGroupResult.propertiesChanged += 'description' + } + + if ($livegroup.name -ne $localgroup.name) + { + $getGroupResult.propertiesChanged += 'displayName' + } + + # If the properties are the same, the group is unchanged. If not, the group has been changed. + if ($getGroupResult.propertiesChanged.count -ne 0) + { + # Update the Result + $getGroupResult.status = [DSCGetSummaryState]::Changed + # Add the reason + } + else + { + # Update the Result + $getGroupResult.status = [DSCGetSummaryState]::Unchanged + } + } + + return $getGroupResult + + } + + # The Group hasn't been renamed. Test the properties to make sure they are the same as the parameters. + + # Compare the properties of the live group with the parameters + if ($livegroup.description -ne $groupDescription) + { + $getGroupResult.propertiesChanged += 'Description' + } + + if ($livegroup.name -ne $localgroup.name) + { + $getGroupResult.propertiesChanged += 'Name' + } + + # If the properties are the same, the group is unchanged. If not, the group has been changed. + $getGroupResult.status = ($getGroupResult.propertiesChanged.count -ne 0) ? [DSCGetSummaryState]::Changed : [DSCGetSummaryState]::Unchanged + if ($getGroupResult.status -ne [DSCGetSummaryState]::Changed) + { + $getGroupResult.Ensure = [Ensure]::Present + } + + # Return the group from the cache + return $getGroupResult + + } + + # If the livegroup is not present and the localgroup is present, the group is missing and recreate it. + if (($null -eq $livegroup) -and ($null -ne $localgroup)) + { + $getGroupResult.status = [DSCGetSummaryState]::NotFound + $getGroupResult.propertiesChanged = @('description', 'displayName') + + return $getGroupResult + } + + <# + If the localgroup is not present and the livegroup is present, the group is not found. Check the properties are the same as the parameters. + If the properties are the same, the group is unchanged. If not, the group has been deleted and then recreated and the new group will become authoritative. + #> + + if (($null -eq $localgroup) -and ($null -ne $livegroup)) + { + + # Validate that the live properties are the same as the parameters + if ($livegroup.description -ne $GroupDescription ) { $getGroupResult.propertiesChanged += 'description' } + if ($livegroup.displayName -ne $GroupName ) { $getGroupResult.propertiesChanged += 'displayName' } + # If the properties are the same, the group is unchanged. If not, the group has been changed. + $getGroupResult.status = ($getGroupResult.propertiesChanged.count -ne 0) ? [DSCGetSummaryState]::Changed : [DSCGetSummaryState]::Unchanged + + if ($getGroupResult.status -ne [DSCGetSummaryState]::Unchanged) + { + # Set the Ensure to Present + $getGroupResult.Ensure = [Ensure]::Present + } else { + # Add the unchanged group to the local cache + Add-CacheItem -Key $Key -Value $livegroup -Type 'Group' + } + + # Return the group from the cache + return $getGroupResult + + } + + # If the livegroup and localgroup are not present, the group is missing and recreate it. + if (($null -eq $livegroup) -and ($null -eq $localgroup)) + { + $getGroupResult.status = [DSCGetSummaryState]::NotFound + $getGroupResult.propertiesChanged = @('description', 'displayName') + + return $getGroupResult + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/New-AzDoProjectGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/New-AzDoProjectGroup.ps1 new file mode 100644 index 000000000..96abb971d --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/New-AzDoProjectGroup.ps1 @@ -0,0 +1,108 @@ +<# +.SYNOPSIS +Creates a new Azure DevOps project group. + +.DESCRIPTION +The New-AzDoProjectGroup function creates a new group within a specified Azure DevOps project. +It requires the project name and group name as mandatory parameters. Optionally, a description +for the group and a lookup result can be provided. The function also supports a force switch +parameter to override existing settings. + +.PARAMETER GroupName +The name of the new group to be created. This parameter is mandatory. + +.PARAMETER GroupDescription +An optional description for the new group. + +.PARAMETER ProjectName +The name of the Azure DevOps project where the group will be created. This parameter is mandatory. + +.PARAMETER LookupResult +An optional hashtable containing lookup results. + +.PARAMETER Ensure +An optional parameter to specify the desired state of the group. + +.PARAMETER Force +A switch parameter to force the creation of the group, overriding any existing settings. + +.EXAMPLE +PS> New-AzDoProjectGroup -GroupName "Developers" -ProjectName "MyProject" + +Creates a new group named "Developers" in the "MyProject" Azure DevOps project. + +.EXAMPLE +PS> New-AzDoProjectGroup -GroupName "Testers" -GroupDescription "QA Team" -ProjectName "MyProject" -Force + +Creates a new group named "Testers" with the description "QA Team" in the "MyProject" Azure DevOps project, +forcing the creation even if the group already exists. + +.NOTES +This function relies on the global variable $Global:DSCAZDO_OrganizationName to construct the API URI. +It also interacts with cache functions like Get-CacheItem, Refresh-CacheIdentity, Add-CacheItem, and Set-CacheObject. +#> +Function New-AzDoProjectGroup +{ + + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter(Mandatory = $true)] + [Alias('Project')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Define parameters for creating a new DevOps group + $params = @{ + GroupName = $GroupName + GroupDescription = $GroupDescription + ApiUri = 'https://vssps.dev.azure.com/{0}' -f $Global:DSCAZDO_OrganizationName + ProjectScopeDescriptor = (Get-CacheItem -Key $ProjectName -Type 'LiveProjects').ProjectDescriptor + } + + # If the project scope descriptor is not found, write a warning message to the console and return. + if ($null -eq $params.ProjectScopeDescriptor) + { + Write-Warning "[New-AzDoProjectGroup] Unable to find project scope descriptor for project '$ProjectName'. Aborting group creation." + return + } + + # Write verbose log before creating a new group + Write-Verbose "[New-AzDoProjectGroup] Creating a new DevOps group with the following parameters: $($params | Out-String)" + + # Create a new group + $group = New-DevOpsGroup @params + + # Write verbose log after group creation + Write-Verbose "[New-AzDoProjectGroup] New DevOps group created: $($group | Out-String)" + + # Update the cache with the new group + Refresh-CacheIdentity -Identity $group -Key $group.principalName -CacheType 'LiveGroups' + + Add-CacheItem -Key $group.principalName -Value $group -Type 'Group' + Write-Verbose "[New-AzDoProjectGroup] Added new group to Group cache with key: $($group.principalName)" + + Set-CacheObject -Content $Global:AzDoGroup -CacheType 'Group' + Write-Verbose "[New-AzDoProjectGroup] Updated global AzDoGroup cache object." + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Remove-AzDoProjectGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Remove-AzDoProjectGroup.ps1 new file mode 100644 index 000000000..f098526d6 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Remove-AzDoProjectGroup.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Removes an Azure DevOps project group. + +.DESCRIPTION +The Remove-AzDoProjectGroup function removes a specified Azure DevOps project group by its name and project. +It also updates the cache to reflect the removal. + +.PARAMETER GroupName +The name of the group to be removed. This parameter is mandatory. + +.PARAMETER GroupDescription +The description of the group to be removed. This parameter is optional. + +.PARAMETER ProjectName +The name of the project that the group belongs to. This parameter is mandatory. + +.PARAMETER LookupResult +A hashtable containing the lookup results for the group. This parameter is optional. + +.PARAMETER Ensure +Specifies whether the group should be present or absent. This parameter is optional. + +.PARAMETER Force +A switch parameter to force the removal of the group without confirmation. This parameter is optional. + +.EXAMPLE +Remove-AzDoProjectGroup -GroupName "Developers" -ProjectName "MyProject" + +This command removes the "Developers" group from the "MyProject" project. + +#> +Function Remove-AzDoProjectGroup +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter(Mandatory = $true)] + [Alias('Project')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # If no cache items exist, return. + if (($null -eq $LookupResult.liveCache) -and ($null -eq $LookupResult.localCache)) + { + return + } + + $params = @{ + GroupDescriptor = $LookupResult.liveCache.Descriptor + ApiUri = 'https://vssps.dev.azure.com/{0}' -f $Global:DSCAZDO_OrganizationName + } + + $cacheItem = @{ + Key = $LookupResult.liveCache.principalName + } + + # If the group is not found, return + if (($null -ne $LookupResult.localCache) -and ($null -eq $LookupResult.liveCache)) + { + $cacheItem.Key = $LookupResult.localCache.principalName + $params.GroupDescriptor = $LookupResult.localCache.Descriptor + } + + # + # Remove the group from the API + $null = Remove-DevOpsGroup @params + + # + # Remove the group from the API + + Remove-CacheItem @cacheItem -Type 'LiveGroups' + Set-CacheObject -Content $Global:AZDOLiveGroups -CacheType 'LiveGroups' + + Remove-CacheItem @cacheItem -Type 'Group' + Set-CacheObject -Content $Global:AzDoGroup -CacheType 'Group' + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Set-AzDoProjectGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Set-AzDoProjectGroup.ps1 new file mode 100644 index 000000000..5760ec00b --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Set-AzDoProjectGroup.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS +Sets or updates an Azure DevOps project group. + +.DESCRIPTION +The Set-AzDoProjectGroup function sets or updates an Azure DevOps project group based on the provided parameters. +It handles renaming, updating group details, and managing cache for the group. + +.PARAMETER GroupName +The name of the Azure DevOps project group. This parameter is mandatory. + +.PARAMETER GroupDescription +The description of the Azure DevOps project group. This parameter is optional. + +.PARAMETER ProjectName +The name of the Azure DevOps project. This parameter is mandatory. + +.PARAMETER LookupResult +A hashtable containing the lookup result for the group. This parameter is optional. + +.PARAMETER Ensure +Specifies whether the group should be present or absent. This parameter is optional. + +.PARAMETER Force +A switch parameter to force the operation. This parameter is optional. + +.EXAMPLE +Set-AzDoProjectGroup -GroupName "Developers" -ProjectName "MyProject" -GroupDescription "Development Team" + +This example sets or updates the "Developers" group in the "MyProject" Azure DevOps project with the description "Development Team". + +.NOTES +If the group has been renamed, a warning is issued and the function returns without making changes. +The function updates both the live and local cache with the new group details. +#> +Function Set-AzDoProjectGroup +{ + param( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$GroupName, + + [Parameter()] + [Alias('Description')] + [System.String]$GroupDescription, + + [Parameter(Mandatory = $true)] + [Alias('Project')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Lookup')] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # + # Depending on the type of lookup status, the group has been renamed the group has been deleted and recreated. + if ($LookupResult.Status -eq [DSCGetSummaryState]::Renamed) + { + # For the time being write a warning and return + Write-Warning "[Set-AzDoProjectGroup] The group has been renamed. The group will not be set." + return + } + + # + # Update the group + $params = @{ + ApiUri = 'https://vssps.dev.azure.com/{0}' -f $Global:DSCAZDO_OrganizationName + GroupName = $GroupName + GroupDescription = $GroupDescription + GroupDescriptor = $LookupResult.liveCache.descriptor + } + + try + { + # Set the group from the API + $group = Set-DevOpsGroup @params + } + catch + { + throw $_ + } + + # + # Firstly Replace the live cache with the new group + + # Update the cache with the new group + Refresh-CacheIdentity -Identity $group -Key $group.principalName -CacheType 'LiveGroups' + + # + # Secondarily Replace the local cache with the new group + if ($null -ne $LookupResult.localCache) + { + Remove-CacheItem -Key $LookupResult.localCache.principalName -Type 'Group' + } + + Add-CacheItem -Key $group.principalName -Value $group -Type 'Group' + Set-CacheObject -Content $Global:AzDoGroup -CacheType 'Group' + + # + # Return the group from the cache + return $group + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Test-AzDoProjectGroup.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Test-AzDoProjectGroup.ps1 new file mode 100644 index 000000000..cb305450c --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Test-AzDoProjectGroup.ps1 @@ -0,0 +1,117 @@ +<# +.SYNOPSIS + Tests if an organization group exists in Azure DevOps. + +.DESCRIPTION + The Test-AzDoOrganizationGroup function checks if a specified organization group exists in Azure DevOps. + It uses a personal access token (PAT) and the Azure DevOps API to perform the check. + +.PARAMETER GroupName + Specifies the name of the organization group to test. + +.PARAMETER Pat + Specifies the personal access token (PAT) to authenticate with Azure DevOps. + The PAT is validated using the Test-AzDevOpsPat function. + +.PARAMETER ApiUri + Specifies the URI of the Azure DevOps API to connect to. + The URI is validated using the Test-AzDevOpsApiUri function. + +.OUTPUTS + System.Boolean + Returns $true if the organization group exists, otherwise returns $false. + +.EXAMPLE + Test-AzDoOrganizationGroup -GroupName 'MyGroup' -Pat '********' -ApiUri 'https://dev.azure.com/myorg' + + Description + ----------- + Tests if the organization group named 'MyGroup' exists in the Azure DevOps organization 'myorg' + using the specified personal access token and API URI. + +#> +Function Test-AzDoProjectGroup +{ + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $GroupName, + + [Parameter()] + [string] + $GroupDescription=$null, + + [Parameter()] + [Alias('Project')] + [hashtable]$ProjectName, + + [Parameter()] + [Alias('Name')] + [hashtable]$GetResult + ) + + # + # Firstly we need to compare to see if the group names are the same. If so we can return $false. + + if ($GetResult.Status -eq [DSCGetSummaryState]::Unchanged ) + { + + $result = $true + + if ($GroupDescription -eq $GetResult.Current.description) + { + $GetResult. + $result = $false + } + + return $true + } + + # If the status has been flagged as 'Renamed', returned $true. This means that the originId has changed. + if ($GetResult.Status -eq [DSCGetSummaryState]::Renamed) + { + return $false + } + + # If the status has been flagged as 'Missing', returned $true. This means that the group is missing from the live cache. + + if ($GetResult.Status -eq [DSCGetSummaryState]::Changed) + { + + # If the group is present in the live cache and the local cache. This means that the originId has changed. This needs to be updated. + if (($null -ne $GetResult.Current) -and ($null -ne $GetResult.Cache)) + { + return $true + } + + # If the group is present in the live cache but not in the local cache. Flag as Changed. + if ($GetResult.Current -and -not($GetResult.Cache)) + { + return $true + } + + # + # If the group is not present in the live cache but is in the local cache. Flag as Changed. + if (-not($GetResult.Current) -and $GetResult.Cache) + { + return $true + } + + } + + # Format the Key According to the Principal Name + $Key = Format-AzDoGroup -Prefix "[$Global:DSCAZDO_OrganizationName]" -GroupName $GroupName + + # Check the cache for the group + $group = Get-CacheItem -Key $Key -Type 'LiveGroups' + if (-not($group)) + { + $false + } + else + { + $true + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Get-AzDoProjectServices.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Get-AzDoProjectServices.ps1 new file mode 100644 index 000000000..d841471c0 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Get-AzDoProjectServices.ps1 @@ -0,0 +1,178 @@ +<# +.SYNOPSIS + Retrieves the status of various Azure DevOps project services. + +.DESCRIPTION + The Get-AzDoProjectServices function retrieves the status of various services (Git Repositories, Work Boards, Build Pipelines, Test Plans, and Azure Artifacts) for a specified Azure DevOps project. It compares the current state of these services with the desired state and returns a summary of the differences. + +.PARAMETER ProjectName + The name of the Azure DevOps project. + +.PARAMETER GitRepositories + The desired state of Git Repositories service. Valid values are 'Enabled' or 'Disabled'. Default is 'Enabled'. + +.PARAMETER WorkBoards + The desired state of Work Boards service. Valid values are 'Enabled' or 'Disabled'. Default is 'Enabled'. + +.PARAMETER BuildPipelines + The desired state of Build Pipelines service. Valid values are 'Enabled' or 'Disabled'. Default is 'Enabled'. + +.PARAMETER TestPlans + The desired state of Test Plans service. Valid values are 'Enabled' or 'Disabled'. Default is 'Enabled'. + +.PARAMETER AzureArtifact + The desired state of Azure Artifacts service. Valid values are 'Enabled' or 'Disabled'. Default is 'Enabled'. + +.PARAMETER LookupResult + A hashtable to store lookup results. + +.PARAMETER Ensure + Specifies whether the project services should be present or absent. + +.PARAMETER Force + Forces the command to run without asking for user confirmation. + +.OUTPUTS + [System.Management.Automation.PSObject[]] + Returns a hashtable containing the status of the project services and any properties that have changed. + +.EXAMPLE + PS C:\> Get-AzDoProjectServices -ProjectName "MyProject" -GitRepositories "Enabled" -WorkBoards "Enabled" -BuildPipelines "Enabled" -TestPlans "Enabled" -AzureArtifact "Enabled" + Retrieves the status of the specified project services for the project "MyProject" and compares them with the desired state. + +.NOTES + This function relies on the presence of a live cache and specific global variables and localized data parameters. +#> +Function Get-AzDoProjectServices +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Repos')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$GitRepositories = 'Enabled', + + [Parameter()] + [Alias('Board')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$WorkBoards = 'Enabled', + + [Parameter()] + [Alias('Pipelines')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$BuildPipelines = 'Enabled', + + [Parameter()] + [Alias('Tests')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$TestPlans = 'Enabled', + + [Parameter()] + [Alias('Artifacts')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$AzureArtifact = 'Enabled', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # + # Construct a hashtable detailing the group + + $Result = @{ + #Reasons = $() + Ensure = [Ensure]::Absent + propertiesChanged = @() + status = [DSCGetSummaryState]::Unchanged + } + + # Attempt to retrive the Project from the Live Cache. + Write-Verbose "[Get-AzDevOpsProjectServices] Retriving the Project from the Live Cache." + + # Retrive the Repositories from the Live Cache. + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + # If the Project does not exist in the Live Cache, return the Project object. + if ($null -eq $Project) + { + Write-Warning "[Get-AzDevOpsProjectServices] The Project '$ProjectName' was not found in the Live Cache." + $Result.Status = [DSCGetSummaryState]::NotFound + return $Result + } + + $params = @{ + Organization = $Global:DSCAZDO_OrganizationName + ProjectId = $Project.id + } + + # Enumerate the Project Services. + $Result.LiveServices = @{ + Repos = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Repos + Boards = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Boards + Pipelines = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Pipelines + Tests = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_TestPlans + Artifacts = Get-ProjectServiceStatus @params -ServiceName $LocalizedDataAzURLParams.ProjectService_Artifacts + } + + # Compare the Project Services with the desired state. + if ($GitRepositories -ne $Result.LiveServices.Repos.state) + { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $GitRepositories + FeatureId = $LocalizedDataAzURLParams.ProjectService_Repos + } + } + + if ($WorkBoards -ne $Result.LiveServices.Boards.state) + { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $WorkBoards + FeatureId = $LocalizedDataAzURLParams.ProjectService_Boards + } + } + + if ($BuildPipelines -ne $Result.LiveServices.Pipelines.state) + { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $BuildPipelines + FeatureId = $LocalizedDataAzURLParams.ProjectService_Pipelines + } + } + + if ($TestPlans -ne $Result.LiveServices.Tests.state) + { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $TestPlans + FeatureId = $LocalizedDataAzURLParams.ProjectService_TestPlans + } + } + + if ($AzureArtifact -ne $Result.LiveServices.Artifacts.state) + { + $Result.Status = [DSCGetSummaryState]::Changed + $Result.propertiesChanged += @{ + Expected = $AzureArtifact + FeatureId = $LocalizedDataAzURLParams.ProjectService_Artifacts + } + } + + return $Result + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/New-AzDoProjectServices.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/New-AzDoProjectServices.ps1 new file mode 100644 index 000000000..454c7fd6c --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/New-AzDoProjectServices.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS +Creates a new Azure DevOps project with specified services. + +.DESCRIPTION +The New-AzDoProjectServices function creates a new Azure DevOps project and configures various services such as Git repositories, work boards, build pipelines, test plans, and Azure artifacts based on the provided parameters. + +.PARAMETER ProjectName +Specifies the name of the Azure DevOps project to be created. This parameter is mandatory. + +.PARAMETER GitRepositories +Specifies whether Git repositories should be enabled or disabled for the project. Default is 'Enabled'. + +.PARAMETER WorkBoards +Specifies whether work boards should be enabled or disabled for the project. Default is 'Enabled'. + +.PARAMETER BuildPipelines +Specifies whether build pipelines should be enabled or disabled for the project. Default is 'Enabled'. + +.PARAMETER TestPlans +Specifies whether test plans should be enabled or disabled for the project. Default is 'Enabled'. + +.PARAMETER AzureArtifact +Specifies whether Azure artifacts should be enabled or disabled for the project. Default is 'Enabled'. + +.PARAMETER LookupResult +A hashtable that can be used to store lookup results. + +.PARAMETER Ensure +Specifies whether the project should be present or absent. + +.PARAMETER Force +If specified, forces the creation of the project even if it already exists. + +.OUTPUTS +System.Management.Automation.PSObject[] + +.EXAMPLE +PS C:\> New-AzDoProjectServices -ProjectName "MyProject" -GitRepositories "Enabled" -WorkBoards "Enabled" -BuildPipelines "Enabled" -TestPlans "Enabled" -AzureArtifact "Enabled" + +Creates a new Azure DevOps project named "MyProject" with all services enabled. +#> +Function New-AzDoProjectServices +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Repos')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$GitRepositories = 'Enabled', + + [Parameter()] + [Alias('Board')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$WorkBoards = 'Enabled', + + [Parameter()] + [Alias('Pipelines')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$BuildPipelines = 'Enabled', + + [Parameter()] + [Alias('Tests')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$TestPlans = 'Enabled', + + [Parameter()] + [Alias('Artifacts')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$AzureArtifact = 'Enabled', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Won't be triggered. + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Remove-AzDoProjectServices.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Remove-AzDoProjectServices.ps1 new file mode 100644 index 000000000..11a9bf1da --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Remove-AzDoProjectServices.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS +Removes specified Azure DevOps project services. + +.DESCRIPTION +The Remove-AzDoProjectServices function removes specified services from an Azure DevOps project. +You can specify which services to remove, such as Git repositories, work boards, build pipelines, +test plans, and Azure artifacts. + +.PARAMETER ProjectName +Specifies the name of the Azure DevOps project from which services will be removed. This parameter is mandatory. + +.PARAMETER GitRepositories +Specifies whether Git repositories should be enabled or disabled. The default value is 'Enabled'. +Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER WorkBoards +Specifies whether work boards should be enabled or disabled. The default value is 'Enabled'. +Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER BuildPipelines +Specifies whether build pipelines should be enabled or disabled. The default value is 'Enabled'. +Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER TestPlans +Specifies whether test plans should be enabled or disabled. The default value is 'Enabled'. +Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER AzureArtifact +Specifies whether Azure artifacts should be enabled or disabled. The default value is 'Enabled'. +Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER LookupResult +A hashtable containing lookup results for the project services. + +.PARAMETER Ensure +Specifies whether to ensure the state of the project services. + +.PARAMETER Force +If specified, forces the removal of the project services without prompting for confirmation. + +.EXAMPLE +Remove-AzDoProjectServices -ProjectName "MyProject" -GitRepositories Disabled -Force + +This command removes the Git repositories from the Azure DevOps project named "MyProject" without prompting for confirmation. + +#> +Function Remove-AzDoProjectServices +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Repos')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$GitRepositories = 'Enabled', + + [Parameter()] + [Alias('Board')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$WorkBoards = 'Enabled', + + [Parameter()] + [Alias('Pipelines')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$BuildPipelines = 'Enabled', + + [Parameter()] + [Alias('Tests')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$TestPlans = 'Enabled', + + [Parameter()] + [Alias('Artifacts')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$AzureArtifact = 'Enabled', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Won't be triggered. + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Set-AzDoProjectServices.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Set-AzDoProjectServices.ps1 new file mode 100644 index 000000000..be2548ea8 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Set-AzDoProjectServices.ps1 @@ -0,0 +1,114 @@ +<# +.SYNOPSIS + Configures the services for an Azure DevOps project. + +.DESCRIPTION + The Set-AzDoProjectServices function enables or disables various services for a specified Azure DevOps project. + It retrieves the project details from the live cache and updates the service status based on the provided parameters. + +.PARAMETER ProjectName + The name of the Azure DevOps project. This parameter is mandatory. + +.PARAMETER GitRepositories + Specifies whether Git repositories should be enabled or disabled. Default is 'Enabled'. + Acceptable values are 'Enabled' and 'Disabled'. + +.PARAMETER WorkBoards + Specifies whether work boards should be enabled or disabled. Default is 'Enabled'. + Acceptable values are 'Enabled' and 'Disabled'. + +.PARAMETER BuildPipelines + Specifies whether build pipelines should be enabled or disabled. Default is 'Enabled'. + Acceptable values are 'Enabled' and 'Disabled'. + +.PARAMETER TestPlans + Specifies whether test plans should be enabled or disabled. Default is 'Enabled'. + Acceptable values are 'Enabled' and 'Disabled'. + +.PARAMETER AzureArtifact + Specifies whether Azure artifacts should be enabled or disabled. Default is 'Enabled'. + Acceptable values are 'Enabled' and 'Disabled'. + +.PARAMETER LookupResult + A hashtable containing the lookup results for the project services. + +.PARAMETER Ensure + Specifies whether to ensure the services are in the desired state. + +.PARAMETER Force + A switch parameter to force the operation. + +.EXAMPLE + Set-AzDoProjectServices -ProjectName "MyProject" -GitRepositories "Enabled" -WorkBoards "Disabled" + +.NOTES + This function requires the Get-CacheItem and Set-ProjectServiceStatus functions to be defined. +#> +Function Set-AzDoProjectServices +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Repos')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$GitRepositories = 'Enabled', + + [Parameter()] + [Alias('Board')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$WorkBoards = 'Enabled', + + [Parameter()] + [Alias('Pipelines')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$BuildPipelines = 'Enabled', + + [Parameter()] + [Alias('Tests')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$TestPlans = 'Enabled', + + [Parameter()] + [Alias('Artifacts')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$AzureArtifact = 'Enabled', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Retrive the Repositories from the Live Cache. + $Project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' + + # Construct a hashtable detailing the group + ForEach ($PropertyChanged in $LookupResult.propertiesChanged) + { + + $params = @{ + Organization = $Global:DSCAZDO_OrganizationName + ProjectId = $Project.id + ServiceName = $PropertyChanged.FeatureId + Body = $LookupResult.LiveServices.Keys | Where-Object { $LookupResult.LiveServices[$_].featureId -eq $PropertyChanged.FeatureId } | ForEach-Object { $LookupResult.LiveServices[$_] } + } + + # Set the Project Service Status + $params.Body.state = ($PropertyChanged.Expected -eq 'Enabled') ? 1 : 0 + + Set-ProjectServiceStatus @params + + } + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Test-AzDoProjectServices.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Test-AzDoProjectServices.ps1 new file mode 100644 index 000000000..630396b04 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectServices/Test-AzDoProjectServices.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS + Tests the Azure DevOps project services configuration. + +.DESCRIPTION + The Test-AzDoProjectServices function checks the configuration of various services within an Azure DevOps project, such as Git repositories, work boards, build pipelines, test plans, and Azure artifacts. + +.PARAMETER ProjectName + The name of the Azure DevOps project to test. + +.PARAMETER GitRepositories + Specifies whether Git repositories are enabled or disabled. Default is 'Enabled'. + Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER WorkBoards + Specifies whether work boards are enabled or disabled. Default is 'Enabled'. + Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER BuildPipelines + Specifies whether build pipelines are enabled or disabled. Default is 'Enabled'. + Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER TestPlans + Specifies whether test plans are enabled or disabled. Default is 'Enabled'. + Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER AzureArtifact + Specifies whether Azure artifacts are enabled or disabled. Default is 'Enabled'. + Valid values are 'Enabled' and 'Disabled'. + +.PARAMETER LookupResult + A hashtable containing lookup results for the project services. + +.PARAMETER Ensure + Specifies whether to ensure the configuration is present or absent. + +.PARAMETER Force + Forces the command to run without asking for user confirmation. + +.OUTPUTS + [System.Management.Automation.PSObject[]] + Returns an array of PSObject representing the status of the project services. + +.EXAMPLE + Test-AzDoProjectServices -ProjectName "MyProject" -GitRepositories "Enabled" -WorkBoards "Enabled" -BuildPipelines "Enabled" -TestPlans "Enabled" -AzureArtifact "Enabled" + This command tests the configuration of the specified Azure DevOps project services for the project named "MyProject". +#> +Function Test-AzDoProjectServices +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject[]])] + param + ( + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String]$ProjectName, + + [Parameter()] + [Alias('Repos')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$GitRepositories = 'Enabled', + + [Parameter()] + [Alias('Board')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$WorkBoards = 'Enabled', + + [Parameter()] + [Alias('Pipelines')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$BuildPipelines = 'Enabled', + + [Parameter()] + [Alias('Tests')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$TestPlans = 'Enabled', + + [Parameter()] + [Alias('Artifacts')] + [ValidateSet('Enabled', 'Disabled')] + [System.String]$AzureArtifact = 'Enabled', + + [Parameter()] + [HashTable]$LookupResult, + + [Parameter()] + [Ensure]$Ensure, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # Won't be triggered. + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsOperation.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsOperation.ps1 index 70bf16a4d..7bf17d04a 100644 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsOperation.ps1 +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsOperation.ps1 @@ -51,7 +51,9 @@ function Get-AzDevOpsOperation Pat = $Pat; ResourceName = 'Operation' } - If(![System.String]::IsNullOrWhiteSpace($OperationId)){ + + if (-not[System.String]::IsNullOrWhiteSpace($OperationId)) + { $azDevOpsApiResourceParameters.ResourceId = $OperationId } @@ -59,9 +61,11 @@ function Get-AzDevOpsOperation [System.Management.Automation.PSObject[]]$apiResources = Get-AzDevOpsApiResource @azDevOpsApiResourceParameters # Filter "Operation" resources - If(![System.String]::IsNullOrWhiteSpace($OperationId)){ + if (-not[System.String]::IsNullOrWhiteSpace($OperationId)) + { $apiResources = $apiResources | Where-Object { $_.id -eq $OperationId } } return [System.Management.Automation.PSObject[]]$apiResources + } diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsProject.ps1 deleted file mode 100644 index 3fdca1c4a..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsProject.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -<# - .SYNOPSIS - Returns an Azure DevOps 'Project' as identified by the 'ProjectId' and/or 'ProjectName' provided. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ProjectId - The 'id' of the 'Project' being obtained/requested. - - .PARAMETER ProjectName - The 'name' of the 'Project' being obtained/requested. Wildcards (e.g. '*') are allowed. - - .EXAMPLE - Get-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' - - Returns all the 'Project' resources (assocated with the Organization/ApiUrl) from Azure DevOps. - - .EXAMPLE - Get-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ProjectName '*' - - Returns all the 'Project' resources (assocated with the Organization/ApiUrl) from Azure DevOps. - - .EXAMPLE - Get-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ProjectId 'YourProjectIdHere' - - Returns the 'Project' resources (assocated with the Organization/ApiUrl) from Azure DevOps related to the 'ProjectId' value provided. - - .EXAMPLE - Get-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ProjectName 'YourProjectNameHere' - - Returns the 'Project' resources (assocated with the Organization/ApiUrl) from Azure DevOps related to the 'ProjectName' value provided. - - .EXAMPLE - Get-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ProjectId 'YourProjectIdHere' -ProjectName 'YourProjectNameHere' - - Returns the 'Project' resources (assocated with the Organization/ApiUrl) from Azure DevOps related to the 'ProjectId' and 'ProjectName' value provided. -#> -function Get-AzDevOpsProject -{ - [CmdletBinding()] - [OutputType([System.Management.Automation.PSObject[]])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter()] - [ValidateScript({ Test-AzDevOpsProjectId -ProjectId $_ -IsValid })] - [Alias('ResourceId','Id')] - [System.String] - $ProjectId, - - [Parameter()] - [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid -AllowWildcard })] - [Alias('Name')] - [System.String] - $ProjectName - ) - - # Prepare initial 'Get-AzDevOpsApiResource' function parameters - $azDevOpsApiResourceParameters = @{ - ApiUri = $ApiUri - Pat = $Pat - ResourceName = 'Project' - } - If(![System.String]::IsNullOrWhiteSpace($ProjectId)){ - $azDevOpsApiResourceParameters.ResourceId = $ProjectId - } - - # Obtain all 'Projects' (Note: This returns a limited set of properties, hence why subsequent calls are made) - [System.Management.Automation.PSObject[]]$apiListResources = Get-AzDevOpsApiResource @azDevOpsApiResourceParameters - [System.Management.Automation.PSObject[]]$projects = @() - - # Filter projects by 'ProjectId' - If(![System.String]::IsNullOrWhiteSpace($ProjectId)){ - $apiListResources = $apiListResources | - Where-Object id -eq $ProjectId - } - - # Filter projects by 'ProjectName' (using 'ilike') - If(![System.String]::IsNullOrWhiteSpace($ProjectName)){ - $apiListResources = $apiListResources | - Where-Object name -ilike $ProjectName - } - - # For each project (if any), call 'Get-AzDevOpsApiResource' again to obtain all 'Project' properties - if ($apiListResources.Count -gt 0) - { - $apiListResources | ForEach-Object { - $azDevOpsApiResourceParameters.ResourceId = $_.id - $projects += $(Get-AzDevOpsApiResource @azDevOpsApiResourceParameters) - } - } - - return [System.Management.Automation.PSObject[]]$projects -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDevOpsProject.ps1 deleted file mode 100644 index a40c71fbc..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDevOpsProject.ps1 +++ /dev/null @@ -1,108 +0,0 @@ -<# - .SYNOPSIS - Creates a new Azure DevOps 'Project' with the specified properties set by the parameters. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ProjectName - The 'name' of the 'Project' being created. - - .PARAMETER ProjectDescription - The 'description' of the 'Project' being created. - - .PARAMETER SourceControlType - The 'sourceControlType' of the 'Project' being created. - - Options are 'Tfvc' or 'Git'. Defaults to 'Git' if no value provided. - - .PARAMETER Force - When this switch is used, any confirmation will be overidden/ignored. - - .EXAMPLE - New-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' ` - -ProjectName 'YourProjectNameHere' ` - -ProjectDescription 'YourProjectDescriptionHere' -SourceControlType 'Git' - - Creates a 'Project' (assocated with the Organization/ApiUrl) in Azure DevOps using project-related, parameter values provided. -#> -function New-AzDevOpsProject -{ - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid })] - [Alias('Name')] - [System.String] - $ProjectName, - - [Parameter()] - [ValidateScript({ Test-AzDevOpsProjectDescription -ProjectDescription $_ -IsValid })] - [AllowEmptyString()] - [Alias('Description')] - [System.String] - $ProjectDescription = '', - - [Parameter()] - [ValidateSet('Git','Tfvc')] - [System.String] - $SourceControlType = 'Git', - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Force - ) - - [string]$resourceJson = ' - { - "id": "00000000-0000-0000-0000-000000000000", - "name": "' + $ProjectName + '", - "description": "' + $ProjectDescription + '", - "capabilities": { - "versioncontrol": { - "sourceControlType": "' + $SourceControlType + '" - }, - "processTemplate": { - "templateTypeId": "6b724908-ef14-45cf-84f8-768b5384da45" - } - } - } -' - - [System.Object]$newResource = $null - $ResourceName = 'Project' - - if ($Force -or $PSCmdlet.ShouldProcess($ApiUri, $ResourceName)) - { - New-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat ` - -ResourceName $ResourceName ` - -Resource $($resourceJson | ConvertFrom-Json) ` - -Force:$Force -Wait | Out-Null - - [System.Object]$newResource = Get-AzDevOpsProject -ApiUri $ApiUri -Pat $Pat ` - -ProjectName $ProjectName - } - - return $newResource -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDoAuthenticationProvider.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDoAuthenticationProvider.ps1 new file mode 100644 index 000000000..db729326a --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDoAuthenticationProvider.ps1 @@ -0,0 +1,148 @@ +<# +.SYNOPSIS +Creates a new Azure Managed Identity. + +.DESCRIPTION +The New-AzDoAuthenticationProvider function creates a new Azure Managed Identity for use in Azure DevOps DSC. + +.PARAMETER OrganizationName +Specifies the name of the organization associated with the Azure Managed Identity. + +.EXAMPLE +New-AzDoAuthenticationProvider -OrganizationName "Contoso" + +This example creates a new Azure Managed Identity for the organization named "Contoso". + +#> +Function New-AzDoAuthenticationProvider +{ + [CmdletBinding(DefaultParameterSetName = 'PersonalAccessToken')] + param ( + # Organization Name + [Parameter(Mandatory = $true, ParameterSetName = 'PersonalAccessToken')] + [Parameter(Mandatory = $true, ParameterSetName = 'SecureStringPersonalAccessToken')] + [Parameter(Mandatory = $true, ParameterSetName = 'ManagedIdentity')] + [Alias('OrgName')] + [String] + $OrganizationName, + + # Personal Access Token + [Parameter(Mandatory = $true, ParameterSetName = 'PersonalAccessToken')] + [Alias('PAT')] + [String] + $PersonalAccessToken, + + # SecureString Personal Access Token + [Parameter(Mandatory = $true, ParameterSetName = 'SecureStringPersonalAccessToken')] + [Alias('SecureStringPAT')] + [SecureString] + $SecureStringPersonalAccessToken, + + # Use Managed Identity + [Parameter(ParameterSetName = 'ManagedIdentity')] + [Switch] + $useManagedIdentity, + + # Don't verify the Token + [Parameter(ParameterSetName = 'ManagedIdentity')] + [Parameter(ParameterSetName = 'PersonalAccessToken')] + [Switch] + $NoVerify, + + # Do not export the Token + # Used by Resources that do not require the Token to be exported. + [Parameter(ParameterSetName = 'PersonalAccessToken')] + [Parameter(ParameterSetName = 'SecureStringPersonalAccessToken')] + [Parameter(ParameterSetName = 'ManagedIdentity')] + [Switch] + $isResource + + ) + + # Test if $ENV:AZDODSC_CACHE_DIRECTORY is set. If not, throw an error. + if ($null -eq $ENV:AZDODSC_CACHE_DIRECTORY) + { + Throw "[New-AzDoAuthenticationProvider] The Environment Variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the Environment Variable 'AZDODSC_CACHE_DIRECTORY' to the Cache Directory." + } + + # Set the Global Variables + $Global:DSCAZDO_OrganizationName = $OrganizationName + $Global:DSCAZDO_AuthenticationToken = $null + + # + # If the parameterset is PersonalAccessToken + if ($PSCmdlet.ParameterSetName -eq 'PersonalAccessToken') + { + + Write-Verbose "[New-AzDoAuthenticationProvider] Creating a new Personal Access Token with OrganizationName $OrganizationName." + + # if the NoVerify switch is not set, verify the Token. + if ($NoVerify) + { + $Global:DSCAZDO_AuthenticationToken = Set-AzPersonalAccessToken -PersonalAccessToken $PersonalAccessToken -OrganizationName $OrganizationName + } + else + { + $Global:DSCAZDO_AuthenticationToken = Set-AzPersonalAccessToken -PersonalAccessToken $PersonalAccessToken -Verify -OrganizationName $OrganizationName + } + + } + # If the parameterset is ManagedIdentity + elseif ($PSCmdlet.ParameterSetName -eq 'ManagedIdentity') + { + + Write-Verbose "[New-AzDoAuthenticationProvider] Creating a new Azure Managed Identity with OrganizationName $OrganizationName." + # If the Token is not Valid. Get a new Token. + + if ($NoVerify) + { + $Global:DSCAZDO_AuthenticationToken = Get-AzManagedIdentityToken -OrganizationName $OrganizationName + } + else + { + $Global:DSCAZDO_AuthenticationToken = Get-AzManagedIdentityToken -OrganizationName $OrganizationName -Verify + } + + } + # If the parameterset is SecureStringPersonalAccessToken + elseif ($PSCmdlet.ParameterSetName -eq 'SecureStringPersonalAccessToken') + { + Write-Verbose "[New-AzDoAuthenticationProvider] Creating a new Personal Access Token with OrganizationName $OrganizationName." + # If the Token is not Valid. Get a new Token. + $Global:DSCAZDO_AuthenticationToken = Set-AzPersonalAccessToken -SecureStringPersonalAccessToken $SecureStringPersonalAccessToken -OrganizationName $OrganizationName + } + + # Export the Token information to the Cache Directory + if ($isResource.IsPresent) + { + Write-Verbose "[New-AzDoAuthenticationProvider] isResource is set. The Token will not be exported." + return + } + + + # Initialize the Cache + Get-AzDoCacheObjects | ForEach-Object { + Initialize-CacheObject -CacheType $_ + } + + # Iterate through Each of the Caching Commands and initalize the Cache. + Get-Command "AzDoAPI_*" | Where-Object Source -eq 'AzureDevOpsDsc.Common' | ForEach-Object { + . $_.Name -OrganizationName $AzureDevopsOrganizationName + } + + # Export the Token to the Cache Directory + + # Create an Object Containing the Organization Name. + $moduleSettingsPath = Join-Path -Path $ENV:AZDODSC_CACHE_DIRECTORY -ChildPath "ModuleSettings.clixml" + Write-Verbose "[New-AzDoAuthenticationProvider] Exporting the Module Settings to $moduleSettingsPath." + + $objectSettings = [PSCustomObject]@{ + OrganizationName = $Global:DSCAZDO_OrganizationName + Token = $Global:DSCAZDO_AuthenticationToken + SecurityDescriptorTypes = Join-Path -Path $ENV:AZDODSC_CACHE_DIRECTORY -ChildPath "SecurityDescriptors.clixml" + } + + # Export the Object to the Cache Directory + $objectSettings | Export-Clixml -LiteralPath $moduleSettingsPath -Depth 5 + +} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Remove-AzDevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Remove-AzDevOpsProject.ps1 deleted file mode 100644 index 266333e6d..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Remove-AzDevOpsProject.ps1 +++ /dev/null @@ -1,65 +0,0 @@ -<# - .SYNOPSIS - Removes/deletes a Azure DevOps 'Project' with the provided 'ProjectId'. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ProjectId - The 'id' of the 'Project' being deleted. - - .PARAMETER Force - When this switch is used, any confirmation will be overidden/ignored. - - .EXAMPLE - New-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' ` - -ProjectName 'YourProjectNameHere' ` - -ProjectDescription 'YourProjectDescriptionHere' -SourceControlType 'Git' - - Creates a 'Project' (assocated with the Organization/ApiUrl) in Azure DevOps using project-related, parameter values provided. -#> -function Remove-AzDevOpsProject -{ - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsProjectId -ProjectId $_ -IsValid })] - [Alias('ResourceId','Id')] - [System.String] - $ProjectId, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Force - ) - - if ($Force -or $PSCmdlet.ShouldProcess($ApiUri, $ResourceName)) - { - Remove-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat ` - -ResourceName 'Project' ` - -ResourceId $ProjectId ` - -Force:$Force -Wait | Out-Null - - } -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Set-AzDevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Set-AzDevOpsProject.ps1 deleted file mode 100644 index 5dbc00229..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Set-AzDevOpsProject.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -<# - .SYNOPSIS - Updates an Azure DevOps 'Project' with the specified properties set by the parameters. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/operations - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ProjectName - The 'name' of the 'Project' being updated. - - .PARAMETER ProjectDescription - The 'description' of the 'Project' being updated. - - .PARAMETER SourceControlType - The 'sourceControlType' of the 'Project' being updated. - - Options are 'Tfvc' or 'Git'. Defaults to 'Git' if no value provided. - - .PARAMETER Force - When this switch is used, any confirmation will be overidden/ignored. - - .EXAMPLE - Set-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' ` - -ProjectName 'YourProjectNameHere' ` - -ProjectDescription 'YourProjectDescriptionHere' -SourceControlType 'Git' - - Creates a 'Project' (assocated with the Organization/ApiUrl) in Azure DevOps using project-related, parameter values provided. -#> -function Set-AzDevOpsProject -{ - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsProjectId -ProjectId $_ -IsValid })] - [Alias('ResourceId','Id')] - [System.String] - $ProjectId, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid })] - [Alias('Name')] - [System.String] - $ProjectName, - - [Parameter()] - [ValidateScript({ Test-AzDevOpsProjectDescription -ProjectDescription $_ -IsValid })] - [AllowEmptyString()] - [Alias('Description')] - [System.String] - $ProjectDescription = '', - - # $SourceControlType - Not supported for updates/set - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Force - ) - -# [string]$resourceJson = ' -# { -# "id": "'+ $ProjectId +'", -# "name": "' + $ProjectName + '", -# "description": "' + $ProjectDescription + '", -# "capabilities": { -# "versioncontrol": { -# "sourceControlType": "' + $SourceControlType + '" -# }, -# "processTemplate": { -# "templateTypeId": "6b724908-ef14-45cf-84f8-768b5384da45" -# } -# } -# } -# ' - - [string]$resourceJson = ' - { - "id": "'+ $ProjectId +'", - "name": "' + $ProjectName + '", - "description": "' + $ProjectDescription + '" - } - ' - - [System.Object]$newResource = $null - $ResourceName = 'Project' - - if ($Force -or $PSCmdlet.ShouldProcess($ApiUri, $ResourceName)) - { - Set-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat ` - -ResourceName $ResourceName ` - -ResourceId $ProjectId ` - -Resource $($resourceJson | ConvertFrom-Json) ` - -Force:$Force -Wait | Out-Null - - [System.Object]$newResource = Get-AzDevOpsProject -ApiUri $ApiUri -Pat $Pat ` - -ProjectId $ProjectId ` - -ProjectName $ProjectName - } - - return $newResource -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Test-AzDevOpsProject.ps1 b/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Test-AzDevOpsProject.ps1 deleted file mode 100644 index ba5e6ace7..000000000 --- a/source/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Test-AzDevOpsProject.ps1 +++ /dev/null @@ -1,87 +0,0 @@ -<# - .SYNOPSIS - Tests the presence of an Azure DevOps API project. - - .PARAMETER ApiUri - The URI of the Azure DevOps API to be connected to. For example: - - https://dev.azure.com/someOrganizationName/_apis/ - - .PARAMETER Pat - The 'Personal Access Token' (PAT) to be used by any subsequent requests/projects - against the Azure DevOps API. This PAT must have the relevant permissions assigned - for the subsequent operations being performed. - - .PARAMETER ProjectId - The 'id' of the Azure DevOps API project. - - .PARAMETER ProjectName - The 'name' of the Azure DevOps API project. - - .EXAMPLE - Test-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ProjectId 'YourProjectId' - - Tests that the Azure DevOps 'Project' (identified by the 'ProjectId') exists. - - .EXAMPLE - Test-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' -ProjectId 'YourProjectName' - - Tests that the Azure DevOps 'Project' (identified by the 'ProjectName') exists. - - .EXAMPLE - Test-AzDevOpsProject -ApiUri 'YourApiUriHere' -Pat 'YourPatHere' ` - -ProjectId 'YourProjectId' -ProjectId 'YourProjectName' - - Tests that the Azure DevOps 'Project' (identified by the 'ProjectId' and 'ProjectName') exists. -#> -function Test-AzDevOpsProject -{ - [CmdletBinding()] - [OutputType([System.Boolean])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateScript( { Test-AzDevOpsApiUri -ApiUri $_ -IsValid })] - [Alias('Uri')] - [System.String] - $ApiUri, - - [Parameter(Mandatory = $true)] - [ValidateScript({ Test-AzDevOpsPat -Pat $_ -IsValid })] - [Alias('PersonalAccessToken')] - [System.String] - $Pat, - - [Parameter(Mandatory = $true, ParameterSetName='ProjectId')] - [Parameter(Mandatory = $true, ParameterSetName='ProjectIdAndProjectName')] - [ValidateScript({ Test-AzDevOpsProjectId -ProjectId $_ -IsValid })] - [Alias('ResourceId','Id')] - [System.String] - $ProjectId, - - [Parameter(Mandatory = $true, ParameterSetName='ProjectName')] - [Parameter(Mandatory = $true, ParameterSetName='ProjectIdAndProjectName')] - [ValidateScript({ Test-AzDevOpsProjectName -ProjectName $_ -IsValid })] - [Alias('Name')] - [System.String] - $ProjectName - ) - - $azDevOpsProjectParameters = @{ - ApiUri = $ApiUri; - Pat = $Pat - } - - If(![string]::IsNullOrWhiteSpace($ProjectId)){ - $azDevOpsProjectParameters.ProjectId = $ProjectId - } - - If(![string]::IsNullOrWhiteSpace($ProjectName)){ - $azDevOpsProjectParameters.ProjectName = $ProjectName - } - - [object[]]$project = Get-AzDevOpsProject @azDevOpsProjectParameters - - - return $($null -ne $project.id) -} diff --git a/source/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Initalize-AzDevOpsCache.ps1 b/source/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Initalize-AzDevOpsCache.ps1 new file mode 100644 index 000000000..c1e030a39 --- /dev/null +++ b/source/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Initalize-AzDevOpsCache.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + Initializes the cache for Azure DevOps resources. + +.DESCRIPTION + This function initializes the cache for Azure DevOps resources by loading the cache from a file for each type. + The cache types include Project, Team, Group, and SecurityDescriptor. + +.PARAMETER None + This function does not accept any parameters. + +.EXAMPLE + Initialize-Cache + Initializes the cache for Azure DevOps resources. + +.NOTES + +#> +function Initialize-Cache +{ + [CmdletBinding()] + param () + + # Write initial verbose message + Write-Verbose "[Initialize-Cache] Starting cache initialization process." + + try + { + # Attempt to load the cache from the file for each type + $cacheTypes = @('Project', 'Team', 'Group', 'SecurityDescriptor', 'LiveGroups', 'LiveProjects') + foreach ($cacheType in $cacheTypes) + { + Write-Verbose "[Initialize-Cache] Initializing cache object of type: $cacheType" + Initialize-CacheObject -CacheType $cacheType + } + + # Confirm completion of cache initialization + Write-Verbose "[Initialize-Cache] Cache initialization process completed successfully." + + } catch + { + throw "[Initialize-Cache] An error occurred during cache initialization: $_" + } +} diff --git a/source/WikiSource/Home.md b/source/WikiSource/Home.md index a1cf946e9..e2d60fa2d 100644 --- a/source/WikiSource/Home.md +++ b/source/WikiSource/Home.md @@ -33,12 +33,20 @@ DSC resources available: Get-DscResource -Module AzureDevOpsDsc ``` +## DSC Resource Documentation + +* [AzDoGitPermission](\Resources\AzDoGitPermission.md) +* [AzDoGitRepository](\Resources\AzDoGitRepository.md) +* [AzDoGroupMember](\Resources\AzDoGroupMember.md) +* [AzDoGroupPermission](\Resources\AzDoGroupPermission.md) +* [AzDoOrganizationGroup](\Resources\AzDoOrganizationGroup.md) +* [AzDoProject](\Resources\AzDoProject.md) +* [AzDoProjectGroup](\Resources\AzDoProjectGroup.md) +* [AzDoProjectServices](\Resources\AzDoProjectServices.md) + ## Prerequisites -The minimum Windows Management Framework (PowerShell) version required is 5.0 -or higher, which ships with Windows 10 or Windows Server 2016, -but can also be installed on Windows 7 SP1, Windows 8.1, Windows Server 2012, -and Windows Server 2012 R2. +The minimum requirement for this module is PowerShell 7.0. ## Change log diff --git a/source/WikiSource/Resources/AzDoGitPermission.md b/source/WikiSource/Resources/AzDoGitPermission.md new file mode 100644 index 000000000..a87b621de --- /dev/null +++ b/source/WikiSource/Resources/AzDoGitPermission.md @@ -0,0 +1,195 @@ +# DSC AzDoGitPermission Resource + +# Syntax + +``` PowerShell +AzDoGitPermission [string] #ResourceName +{ + ProjectName = [String]$ProjectName + RepositoryName = [String]$RepositoryName + Permissions = [HashTable]$Permissions # See Permissions Syntax + [ Ensure = [String] {'Present', 'Absent'}] +} +``` + +## Permissions Syntax + +``` PowerShell +AzDoGitPermission/Permissions +{ + Identity = [String]$Identity # Syntax + # SYNTAX: '[ProjectName | OrganizationName]\ServicePrincipalName, UserPrincipalName, UserDisplayName, GroupDisplayName' + # EXAMPLE: '[TestProject]\UserName@email.com' + # EXAMPLE: '[SampleOrganizationName]\Project Collection Administrators' + Permission = [Hashtable[]]$Permissions # See 'Permission List" +} +``` + +## Permission Usage + +``` PowerShell +AzDoGitPermission/Permissions/Permission +{ + PermissionName|PermissionDisplayName = [String]$Name { 'Allow, Deny' } +} + +``` + +## Permission List + +> Either 'Name' or 'DisplayName' can be used + +| Name | DisplayName | Values | Note | +| ------------- | ------------- | - | - | +|Administer | Administer | [ allow, deny ] | Not recommended. | +|GenericRead | Read | [ allow, deny ] | | +|GenericContribute | Contribute | [ allow, deny ] | | +|ForcePush | Force push (rewrite history, delete branches and tags) | [ allow, deny ] | | +|CreateBranch | Create branch |[ allow, deny ] | | +|CreateTag | Create tag | [ allow, deny ] | | +|ManageNote | Manage notes | [ allow, deny ] | | +|PolicyExempt | Bypass policies when pushing | [ allow, deny ] | | +|CreateRepository | Create repository | [ allow, deny ] | | +|DeleteRepository | Delete or disable repository | [ allow, deny ] | | +|RenameRepository | Rename repository | [ allow, deny ] | | +|EditPolicies | Edit policies | [ allow, deny ] | | +|RemoveOthersLocks | Remove others' locks | [ allow, deny ] | | +|ManagePermissions | Manage permissions | [ allow, deny ] | | +|PullRequestContribute | Contribute to pull requests | [ allow, deny ] | | +|PullRequestBypassPolicy | Bypass policies when completing pull requests | [ allow, deny ] | | +|ViewAdvSecAlerts | Advanced Security: view alerts | [ allow, deny ] | | +|DismissAdvSecAlerts | Advanced Security: manage and dismiss alerts | [ allow, deny ] | | +|ManageAdvSecScanning | Advanced Security: manage settings | [ allow, deny ] | | + +# Common Properties + +- __ProjectName__: The name of the Azure DevOps project. +- __RepositoryName__: The name of the Git repository within the project. +- __Permissions__: A HashTable that specifies the permissions to be set. Refer to: 'Permissions Syntax'. +- __Ensure__: Specifies whether the repository should exist. Defaults to 'Absent'. + +# Additional Information + +This resource allows you to manage Azure DevOps projects using Desired State Configuration (DSC). +It includes properties for specifying the project name, description, source control type, process template, and visibility. + +# Examples + +## Example 1: Sample Configuration using AzDoGitPermission Resource + +``` PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoGitPermission GitPermission { + Ensure = 'Present' + ProjectName = 'SampleProject' + RepositoryName = 'SampleGitRepository' + isInherited = $true + Permissions = @( + @{ + Identity = '[ProjectName]\GroupName' + Permissions = @{ + Read = 'Allow' + "Manage Notes" = 'Allow' + "Contribute" = 'Deny' + } + } + ) + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose + +``` + +## Example 2: Sample Configuration using Invoke-DSCResource + +``` PowerShell +# Return the current configuration for AzDoGitPermission +# Ensure is not required +$properties = @{ + ProjectName = 'SampleProject' + RepositoryName = 'SampleGitRepository' + isInherited = $true + Permissions = @( + @{ + Identity = '[ProjectName]\GroupName' + Permissions = @{ + Read = 'Allow' + "Manage Notes" = 'Allow' + "Contribute" = 'Deny' + } + } + ) +} + +Invoke-DSCResource -Name 'AzDoGitPermission' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 3: Sample Configuration to clear permissions for an identity within a group + +``` PowerShell +# Remove all group members from the group. +$properties = @{ + ProjectName = 'SampleProject' + RepositoryName = 'SampleGitRepository' + isInherited = $true + Permissions = @( + @{ + Identity = '[ProjectName]\GroupName' + Permissions = @{} + } + ) +} + +Invoke-DSCResource -Name 'AzDoGitPermission' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 4: Sample Configuration using AzDO-DSC-LCM + +``` YAML +parameters: {} + +variables: { + ProjectName: SampleProject, + RepositoryName: SampleRepository +} + +resources: + + - name: SampleGroup Permissions + type: AzureDevOpsDsc/AzDoGitPermission + dependsOn: + - AzureDevOpsDsc/AzDoProjectGroup/SampleGroupReadAccess + properties: + projectName: $ProjectName + RepositoryName: $RepositoryName + isInherited: false + Permissions: + - Identity: '[$ProjectName]\SampleGroupReadAccess' + Permission: + Read: "Allow" + "Manage notes": "Allow" +``` + +LCM Initialization: + +``` PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params + +``` diff --git a/source/WikiSource/Resources/AzDoGitRepository.md b/source/WikiSource/Resources/AzDoGitRepository.md new file mode 100644 index 000000000..2dbe24069 --- /dev/null +++ b/source/WikiSource/Resources/AzDoGitRepository.md @@ -0,0 +1,83 @@ +# DSC AzDoGitRepository Resource + +## Syntax + +```PowerShell +AzDoGitRepository [string] #ResourceName +{ + ProjectName = [String]$ProjectName + RepositoryName = [String]$RepositoryName + [ SourceRepository = [String]$SourceRepository ] + [ Ensure = [String] {'Present', 'Absent'}] +} +``` + +## Permissions Syntax + +This resource does not directly manage permissions. It focuses on managing Git repositories within an Azure DevOps project. + +## Permission Usage + +Not applicable for this resource. + +## Permission List + +Not applicable for this resource. + +## Common Properties + +- __Ensure__: Specifies whether the repository should exist. Defaults to 'Absent'. +- __ProjectName__: The name of the Azure DevOps project. +- __RepositoryName__: The name of the Git repository within the project. +- __SourceRepository__: (Optional) The source repository from which to create the new repository. + +## Additional Information + +This resource allows you to manage Git repositories in Azure DevOps projects using Desired State Configuration (DSC). It includes properties for specifying the project name, repository name, and optionally a source repository. + +## Examples + +### Example 1: Create a Git Repository + +```PowerShell +Configuration Sample_AzDoGitRepository +{ + Import-DscResource -ModuleName AzDevOpsDsc + + Node localhost + { + AzDoGitRepository MyRepository + { + ProjectName = 'MySampleProject' + RepositoryName = 'MySampleRepository' + SourceRepository = 'TemplateRepository' + Ensure = 'Present' + } + } +} + +Sample_AzDoGitRepository -OutputPath 'C:\DSC\' +Start-DscConfiguration -Path 'C:\DSC\' -Wait -Verbose -Force +``` + +### Example 2: Remove a Git Repository + +```PowerShell +Configuration Remove_AzDoGitRepository +{ + Import-DscResource -ModuleName AzDevOpsDsc + + Node localhost + { + AzDoGitRepository MyRepository + { + ProjectName = 'MySampleProject' + RepositoryName = 'MySampleRepository' + Ensure = 'Absent' + } + } +} + +Remove_AzDoGitRepository -OutputPath 'C:\DSC\' +Start-DscConfiguration -Path 'C:\DSC\' -Wait -Verbose -Force +``` diff --git a/source/WikiSource/Resources/AzDoGroupMember.md b/source/WikiSource/Resources/AzDoGroupMember.md new file mode 100644 index 000000000..15c1f7172 --- /dev/null +++ b/source/WikiSource/Resources/AzDoGroupMember.md @@ -0,0 +1,127 @@ +# DSC AzDoGroupMember Resource + +## Syntax + +```PowerShell +AzDoGroupMember [string] #ResourceName +{ + GroupName = [String]$GroupName # [ProjectName|OrganizationName]\GroupName + # For GroupMember Syntax, refer to # GroupMembers Syntax + [ GroupMembers = [String[]]$GroupMembers ] +} +``` + +### GroupMembers Syntax + +``` PowerShell +{ + GroupMember = [String]$GroupMemberName # [ProjectName|OrganizationName]\GroupName +} +``` + +The following string represents the service accounts for the project collection in Azure DevOps Organization: + +```text +[ProjectName|AZDOOrganizationName]\Project Collection Service Accounts +``` + +- __[ProjectName|AZDOOrganizationName]__: The AzDO Project or Organizational Name. +- __Project Collection Service Accounts__: The Group Member Name. This can be a Group Name, Service Principal Name or Service Principle. + +#### Example + +If your Azure DevOps Organization name is `MyOrg`, the string would look like: + +```text +[MyOrg]\Project Collection Service Accounts +``` + +## Properties + +Common Properties: + +- __GroupName__: The name of the Azure DevOps group. +- __GroupMembers__: An array of members to be included in the Azure DevOps group. + +## Additional Information + +This resource is used to manage Azure DevOps group memberships using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps group and ensures that the group is configured according to those properties. + +## Examples + +### Example 1: Sample Configuration for Azure DevOps Group using AzDoGroupMember Resource + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoGroupMember GroupExample { + GroupName = 'MySampleGroup' + GroupMembers = @('user1@example.com', 'user2@example.com') + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Sample Configuration for Azure DevOps Group using Invoke-DSCResource + +```PowerShell +# Return the current configuration for AzDoGroupMember +$properties = @{ + GroupName = 'MySampleGroup' + GroupMembers = @('user1@example.com', 'user2@example.com') +} + +Invoke-DSCResource -Name 'AzDoGroupMember' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 3: Sample Configuration to remove/exclude an Azure DevOps Group using Invoke-DSCResource + +```PowerShell +# Remove the Azure DevOps Group and ensure that it is not recreated. +$properties = @{ + GroupName = 'MySampleGroup' + Ensure = 'Absent' +} + +Invoke-DSCResource -Name 'AzDoGroupMember' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 4: Sample Configuration using AzDO-DSC-LCM + +```YAML +parameters: {} + +variables: { + GroupName: SampleGroup, + GroupMembers: ['user1@example.com', 'user2@example.com'] +} + +resources: + + - name: Group + type: AzureDevOpsDsc/AzDoGroupMember + properties: + groupName: $GroupName + groupMembers: $GroupMembers +``` + +## LCM Initialization + +```PowerShell +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params +``` diff --git a/source/WikiSource/Resources/AzDoGroupPermission.md b/source/WikiSource/Resources/AzDoGroupPermission.md new file mode 100644 index 000000000..990575577 --- /dev/null +++ b/source/WikiSource/Resources/AzDoGroupPermission.md @@ -0,0 +1,147 @@ +# AzDoGroupPermission Resource Documentation (Currently Disabled) + +## Overview + +The `AzDoGroupPermission` resource is part of the Azure DevOps Desired State Configuration (DSC) module. It allows you to manage group permissions within an Azure DevOps project repository. This resource provides properties for specifying the group name, permission inheritance, and a list of permissions to be set. + +## Syntax + +```PowerShell +AzDoGroupPermission [string] #ResourceName +{ + GroupName = [String]$GroupName + [ isInherited = [Boolean]$isInherited ] + [ Permissions = [HashTable[]]$Permissions ] +} +``` + +### Properties + +- **GroupName**: The name of the Azure DevOps group. This property is mandatory. +- **isInherited**: Specifies whether the permissions should be inherited. Defaults to `$true`. +- **Permissions**: A HashTable array that specifies the permissions to be set for the group. Refer to the 'Permissions Syntax' section below. + +## Permissions Syntax + +```PowerShell +AzDoGroupPermission/Permissions +{ + Identity = [String]$Identity + # SYNTAX: '[ProjectName | OrganizationName]\ServicePrincipalName, UserPrincipalName, UserDisplayName, GroupDisplayName' + # ALTERNATIVE SYNTAX: 'this' Referring to the group. + # EXAMPLE: '[TestProject]\UserName@email.com' + # EXAMPLE: '[SampleOrganizationName]\Project Collection Administrators' + Permission = [Hashtable[]]$Permissions +} +``` + +### Permission Usage + +```PowerShell +AzDoGroupPermission/Permissions/Permission +{ + PermissionName|PermissionDisplayName = [String]$Name { 'Allow, Deny' } +} +``` + +### Permission List + +Either 'Name' or 'DisplayName' can be used: + +| Name | DisplayName | Values | Note | +|-------------------------|------------------------------------------------------|-----------------|------------------| +| Read | View identity information | [ allow, deny ] | | +| Write | Edit identity information | [ allow, deny ] | | +| Delete | Delete identity information | [ allow, deny ] | | +| ManageMembership | Manage group membership | [ allow, deny ] | | +| CreateScope | Create identity scopes | [ allow, deny ] | | +| RestoreScope | Restore identity scopes | [ allow, deny ] | | + +## Examples + +### Example 1: Set Group Permissions + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoGroupPermission GroupPermission { + GroupName = 'SampleGroup' + isInherited = $true + Permissions = @( + @{ + Identity = '[SampleProject]\SampleGroup' + Permissions = @{ + "Read" = 'Allow' + "Write" = 'Allow' + "Delete" = 'Deny' + } + } + ) + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Clear Group Permissions + +```PowerShell +# Remove all permissions from the group. +$properties = @{ + GroupName = 'SampleGroup' + isInherited = $true + Permissions = @( + @{ + Identity = '[SampleProject]\SampleGroup' + Permissions = @{} + } + ) +} + +Invoke-DSCResource -Name 'AzDoGroupPermission' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Methods + +### Get Method + +Retrieves the current state properties of the `AzDoGroupPermission` resource. + +```PowerShell +[AzDoGroupPermission] Get() +{ + return [AzDoGroupPermission]$($this.GetDscCurrentStateProperties()) +} +``` + +### GetDscCurrentStateProperties Method + +Returns the current state properties of the resource object. + +```PowerShell +hidden [Hashtable] GetDscCurrentStateProperties([PSCustomObject]$CurrentResourceObject) +{ + $properties = @{ + Ensure = [Ensure]::Absent + } + + if ($null -eq $CurrentResourceObject) + { + return $properties + } + + $properties.GroupName = $CurrentResourceObject.GroupName + $properties.isInherited = $CurrentResourceObject.isInherited + $properties.Permissions = $CurrentResourceObject.Permissions + + Write-Verbose "[AzDoGroupPermission] Current state properties: $($properties | Out-String)" + + return $properties +} +``` + +This class inherits from the `AzDevOpsDscResourceBase` class, which provides the base functionality for DSC resources in the Azure DevOps DSC module. diff --git a/source/WikiSource/Resources/AzDoOrganizationGroup.md b/source/WikiSource/Resources/AzDoOrganizationGroup.md new file mode 100644 index 000000000..d9b4d11ca --- /dev/null +++ b/source/WikiSource/Resources/AzDoOrganizationGroup.md @@ -0,0 +1,96 @@ +# DSC AzDoOrganizationGroup Resource + +## Syntax + +```PowerShell +AzDoOrganizationGroup [string] #ResourceName +{ + GroupName = [String]$GroupName + [ GroupDescription = [String]$GroupDescription ] +} +``` + +## Properties + +### Common Properties + +- **GroupName**: The name of the organization group. This property is mandatory and serves as the key property for the resource. +- **GroupDescription**: A description of the organization group. + +## Additional Information + +This resource is used to manage Azure DevOps organization groups using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps organization group and ensures that the group is configured according to those properties. + +## Examples + +## Example 1: Sample Configuration using AzDoOrganizationGroup Resource + +``` PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoOrganizationGroup OrgGroup { + Ensure = 'Present' + GroupName = 'SampleGroup' + GroupDescription = 'This is a sample group!' + } + } +} + +OrgGroup +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose + +``` + +## Example 2: Sample Configuration using Invoke-DSCResource + +``` PowerShell +# Return the current configuration for AzDoGitPermission +# Ensure is not required +$properties = @{ + GroupName = 'SampleGroup' + GroupDescription = 'This is a sample group!' +} + +Invoke-DSCResource -Name 'AzDoOrganizationGroup' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 3: Sample Configuration using AzDO-DSC-LCM + +``` YAML +parameters: {} + +variables: { + "PlaceHolder2": "PlaceHolder" +} + +resources: +- name: Team Leaders Organization Group + type: AzureDevOpsDsc/AzDoOrganizationGroup + properties: + GroupName: AZDO_TeamLeaders_Group + GroupDescription: Team Leaders Organization Group + +- name: Service Accounts Organization Group + type: AzureDevOpsDsc/AzDoOrganizationGroup + properties: + GroupName: AZDO_ServiceAccounts_Group + GroupDescription: Service Accounts Organization Group +``` + +LCM Initialization: + +``` PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params diff --git a/source/WikiSource/Resources/AzDoProject.md b/source/WikiSource/Resources/AzDoProject.md new file mode 100644 index 000000000..dc3ee6168 --- /dev/null +++ b/source/WikiSource/Resources/AzDoProject.md @@ -0,0 +1,123 @@ +# DSC AzDoProject Resource + +# Syntax + +``` PowerShell +AzDoProject [string] #ResourceName +{ + ProjectName = [String]$ProjectName + [ Ensure = [String] {'Present', 'Absent'}] + [ ProjectDescription = [String]$ProjectDescription] + [ SourceControlType = [String] {'Git', 'Tfvc'}] + [ ProcessTemplate = [String] {'Agile', 'Scrum', 'CMMI', 'Basic'}] + [ Visibility = [String] {'Public', 'Private'}] +} +``` + +# Properties + +Common Properties: + +- __ProjectName__: The name of the Azure DevOps project. +- __ProjectDescription__: A description for the Azure DevOps project. +- __SourceControlType__: The type of source control (Git or Tfvc). Default is Git. +- __ProcessTemplate__: The process template to use (Agile, Scrum, CMMI, Basic). Default is Agile. +- __Visibility__: The visibility of the project (Public or Private). Default is Private. + +# Additional Information + +This resource is used to manage Azure DevOps projects using Desired State Configuration (DSC). +It allows you to define the properties of an Azure DevOps project and ensures that the project is configured according to those properties. + +# Examples + +## Example 1: Sample Configuration for Azure DevOps Project using AzDoProject Resource + +``` PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoProject ProjectExample { + Ensure = 'Present' + ProjectName = 'MySampleProject' + ProjectDescription = 'This is a sample Azure DevOps project.' + SourceControlType = 'Git' + ProcessTemplate = 'Agile' + Visibility = 'Private' + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose + +``` + +## Example 2: Sample Configuration for Azure DevOps Project using Invoke-DSCResource + +``` PowerShell +# Return the current configuration for AzDoProject +# Ensure is not required +$properties = @{ + ProjectName = 'MySameProject' + ProjectDiscription = 'This is a sample Azure DevOps project' + SourceControlType = 'Git' + ProcessTemplate = 'Agile' + Visibility = 'Private' +} + +Invoke-DSCResource -Name 'AzDoProject' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 3: Sample Configuration to remove/exclude an Azure DevOps Project using Invoke-DSCResource + +``` PowerShell +# Remove the Azure Devops Project and ensure that it is not recreated. +$properties = @{ + ProjectName = 'MySameProject' + Ensure = 'Absent' +} + +Invoke-DSCResource -Name 'AzDoProject' -Method Set -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +## Example 4: Sample Configuration using AzDO-DSC-LCM + +``` YAML +parameters: {} + +variables: { + ProjectName: SampleProject, + ProjectDescription: This is a SampleProject! +} + +resources: + + - name: Project + type: AzureDevOpsDsc/AzDoProject + properties: + projectName: $ProjectName + projectDescription: $ProjectDescription + visibility: private + SourceControlType: Git + ProcessTemplate: Agile +``` + +LCM Initialization: + +``` PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params + +``` diff --git a/source/WikiSource/Resources/AzDoProjectGroup.md b/source/WikiSource/Resources/AzDoProjectGroup.md new file mode 100644 index 000000000..eeeb148ba --- /dev/null +++ b/source/WikiSource/Resources/AzDoProjectGroup.md @@ -0,0 +1,102 @@ +# DSC AzDoProjectGroup Resource + +## Syntax + +```PowerShell +AzDoProjectGroup [string] #ResourceName +{ + GroupName = [String]$GroupName + ProjectName = [String]$ProjectName + [ GroupDescription = [String]$GroupDescription ] +} +``` + +## Properties + +### Common Properties + +- **GroupName**: The name of the project group. This property is mandatory and serves as the key property for the resource. +- **ProjectName**: The name of the Azure DevOps project associated with this group. This property is mandatory. +- **GroupDescription**: A description of the project group. + +## Additional Information + +This resource is used to manage Azure DevOps project groups using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps project group and ensures that the group is configured according to those properties. + +## Examples + +### Example 1: Sample Configuration using AzDoProjectGroup Resource + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoProjectGroup ProjectGroup { + Ensure = 'Present' + GroupName = 'SampleProjectGroup' + ProjectName = 'SampleProject' + GroupDescription = 'This is a sample project group!' + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Sample Configuration using Invoke-DSCResource + +```PowerShell +# Return the current configuration for AzDoProjectGroup +# Ensure is not required +$properties = @{ + GroupName = 'SampleProjectGroup' + ProjectName = 'SampleProject' + GroupDescription = 'This is a sample project group!' +} + +Invoke-DSCResource -Name 'AzDoProjectGroup' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 3: Sample Configuration using AzDO-DSC-LCM + +```YAML +parameters: {} + +variables: { + "PlaceHolder2": "PlaceHolder" +} + +resources: +- name: Team Leaders Project Group + type: AzureDevOpsDsc/AzDoProjectGroup + properties: + GroupName: AZDO_TeamLeaders_ProjectGroup + ProjectName: SampleProject + GroupDescription: Team Leaders Project Group + +- name: Service Accounts Project Group + type: AzureDevOpsDsc/AzDoProjectGroup + properties: + GroupName: AZDO_ServiceAccounts_ProjectGroup + ProjectName: SampleProject + GroupDescription: Service Accounts Project Group +``` + +LCM Initialization: + +```PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params +``` diff --git a/source/WikiSource/Resources/AzDoProjectServices.md b/source/WikiSource/Resources/AzDoProjectServices.md new file mode 100644 index 000000000..e44f21af3 --- /dev/null +++ b/source/WikiSource/Resources/AzDoProjectServices.md @@ -0,0 +1,111 @@ +# DSC AzDoProjectServices Resource + +## Syntax + +```PowerShell +AzDoProjectServices [string] #ResourceName +{ + ProjectName = [String]$ProjectName + [ GitRepositories = [String]$GitRepositories { 'Enabled' | 'Disabled' } ] + [ WorkBoards = [String]$WorkBoards { 'Enabled' | 'Disabled' } ] + [ BuildPipelines = [String]$BuildPipelines { 'Enabled' | 'Disabled' } ] + [ TestPlans = [String]$TestPlans { 'Enabled' | 'Disabled' } ] + [ AzureArtifact = [String]$AzureArtifact { 'Enabled' | 'Disabled' } ] +} +``` + +## Properties + +### Common Properties + +- **ProjectName**: The name of the Azure DevOps project. This property is mandatory and serves as the key property for the resource. +- **GitRepositories**: Specifies whether Git repositories are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **WorkBoards**: Specifies whether work boards are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **BuildPipelines**: Specifies whether build pipelines are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **TestPlans**: Specifies whether test plans are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. +- **AzureArtifact**: Specifies whether Azure artifacts are enabled or disabled. Valid values are `Enabled` or `Disabled`. Default is `Enabled`. + +## Additional Information + +This resource is used to manage Azure DevOps project services using Desired State Configuration (DSC). It allows you to define the properties of an Azure DevOps project and ensures that the services are configured according to those properties. + +## Examples + +### Example 1: Sample Configuration using AzDoProjectServices Resource + +```PowerShell +Configuration ExampleConfig { + Import-DscResource -ModuleName 'AzDevOpsDsc' + + Node localhost { + AzDoProjectServices ProjectServices { + Ensure = 'Present' + ProjectName = 'SampleProject' + GitRepositories = 'Enabled' + WorkBoards = 'Enabled' + BuildPipelines = 'Enabled' + TestPlans = 'Enabled' + AzureArtifact = 'Enabled' + } + } +} + +ExampleConfig +Start-DscConfiguration -Path ./ExampleConfig -Wait -Verbose +``` + +### Example 2: Sample Configuration using Invoke-DSCResource + +```PowerShell +# Return the current configuration for AzDoProjectServices +# Ensure is not required +$properties = @{ + ProjectName = 'SampleProject' + GitRepositories = 'Enabled' + WorkBoards = 'Enabled' + BuildPipelines = 'Enabled' + TestPlans = 'Enabled' + AzureArtifact = 'Enabled' +} + +Invoke-DSCResource -Name 'AzDoProjectServices' -Method Get -Property $properties -ModuleName 'AzureDevOpsDsc' +``` + +### Example 3: Sample Configuration using AzDO-DSC-LCM + +```YAML +parameters: {} + +variables: { + "PlaceHolder2": "PlaceHolder" +} + +resources: +- name: Sample Project Services + type: AzureDevOpsDsc/AzDoProjectServices + properties: + ProjectName: SampleProject + GitRepositories: Enabled + WorkBoards: Enabled + BuildPipelines: Enabled + TestPlans: Enabled + AzureArtifact: Enabled +``` + +LCM Initialization: + +```PowerShell + +$params = @{ + AzureDevopsOrganizationName = "SampleAzDoOrgName" + ConfigurationDirectory = "C:\Datum\DSCOutput\" + ConfigurationUrl = 'https://configuration-path' + JITToken = 'SampleJITToken' + Mode = 'Set' + AuthenticationType = 'ManagedIdentity' + ReportPath = 'C:\Datum\DSCOutput\Reports' +} + +Invoke-AzDoLCM @params +``` + diff --git a/tests/Integration/DSCClassResources/AzDevOpsProject.Integration.Tests.ps1 b/tests/Integration/DSCClassResources/Archive/AzDevOpsProject.Integration.Tests.ps1 similarity index 100% rename from tests/Integration/DSCClassResources/AzDevOpsProject.Integration.Tests.ps1 rename to tests/Integration/DSCClassResources/Archive/AzDevOpsProject.Integration.Tests.ps1 diff --git a/tests/Integration/DSCClassResources/AzDevOpsProject.config.ps1 b/tests/Integration/DSCClassResources/Archive/AzDevOpsProject.config.ps1 similarity index 100% rename from tests/Integration/DSCClassResources/AzDevOpsProject.config.ps1 rename to tests/Integration/DSCClassResources/Archive/AzDevOpsProject.config.ps1 diff --git a/tests/Integration/Invoke-Tests.ps1 b/tests/Integration/Invoke-Tests.ps1 new file mode 100644 index 000000000..44bfdf8bf --- /dev/null +++ b/tests/Integration/Invoke-Tests.ps1 @@ -0,0 +1,29 @@ +#Requires -Modules @{ ModuleName="Pester"; ModuleVersion="5.0.0" } +param( + [Parameter(Mandatory = $true)] + [String]$TestFrameworkConfigurationPath +) + +# +# Dot Source the Supporting Functions + +$CurrentLocation = Get-Location + +Get-ChildItem -Path "$($CurrentLocation.Path)\Supporting\Functions" -Filter "*.ps1" | ForEach-Object { . $_.FullName } + +# +# Firstly Initialize the test environment +. "$($CurrentLocation.Path)\Supporting\Initalize-TestFramework.ps1" -TestFrameworkConfigurationPath $TestFrameworkConfigurationPath + +# +# Trigger the Tests + +Invoke-Pester -Path "$PSScriptRoot\Resources" + +# +# Tear down the test environment + +Get-ChildItem -Path "$($CurrentLocation.Path)\Supporting\API" -Filter "*.ps1" | ForEach-Object { . $_.FullName } +Get-ChildItem -Path "$($CurrentLocation.Path)\Supporting\APICalls" -Filter "*.ps1" | ForEach-Object { . $_.FullName } + +. "$($CurrentLocation.Path)\Supporting\Teardown.ps1" -ClearAll -OrganizationName $GLOBAL:DSCAZDO_OrganizationName -TestFrameworkConfiguration $TestFrameworkConfiguration diff --git a/tests/Integration/Resources/AzDoGitPermission.tests.ps1 b/tests/Integration/Resources/AzDoGitPermission.tests.ps1 new file mode 100644 index 000000000..9a3820b69 --- /dev/null +++ b/tests/Integration/Resources/AzDoGitPermission.tests.ps1 @@ -0,0 +1,144 @@ +Describe "AzDoGitPermission Integration Tests" { + + BeforeAll { + + # Perform setup tasks here + $PROJECTNAME = 'TESTPROJECT_GIT_PERMISSION' + + $parameters = @{ + Name = 'AzDoGitPermission' + ModuleName = 'AzureDevOpsDsc' + property = @{ + ProjectName = $PROJECTNAME + RepositoryName = 'TESTREPOSITORY' + isInherited = $false + Permissions = @( + @{ + Identity = "[$PROJECTNAME]\Group1" + Permission = @{ + Read = 'Allow' + Write = 'Allow' + } + } + @{ + Identity = "[$PROJECTNAME]\Group2" + Permission = @{ + Read = 'Deny' + Write = 'Deny' + } + } + ) + } + } + + # + # Create a new project + New-Project $PROJECTNAME + + # + # Create a new repository + + New-Repository -ProjectName $PROJECTNAME -RepositoryName 'TESTREPOSITORY' + + # + # Create some new groups + + 'Group1', 'Group2' | ForEach-Object { + New-Group -ProjectName $PROJECTNAME -GroupName $_ + } + } + + Context "Testing if the permissions exist" { + + BeforeAll { + $parameters.Method = 'Test' + } + + It "Should not throw any exceptions" { + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return False" { + $result = Invoke-DscResource @parameters + $result.InDesiredState | Should -BeFalse + } + + } + + Context "Creating new permissions" { + + BeforeAll { + $parameters.Method = 'Set' + } + + It "Should not throw any exceptions" { + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + # Set up the parameters for the DSC resource invocation. + $parameters.Method = 'Test' + $result = Invoke-DscResource @parameters + $result.InDesiredState | Should -BeTrue + } + + } + + Context "Changing permissions" { + + BeforeAll { + $parameters.Method = 'Set' + $parameters.property.Permissions = @( + @{ + Identity = "[$PROJECTNAME]\Group1" + Permission = @{ + Read = 'Allow' + Write = 'Deny' + } + } + @{ + Identity = "[$PROJECTNAME]\Group2" + Permission = @{ + Read = 'Deny' + Write = 'Allow' + } + } + ) + } + + It "Should not throw any exceptions" { + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + # Set up the parameters for the DSC resource invocation. + $parameters.Method = 'Test' + + $result = Invoke-DscResource @parameters + $result.InDesiredState | Should -BeTrue + } + } + + Context "Clearing permissions should revert to inherited" { + + BeforeAll { + $parameters.Method = 'Set' + $parameters.property.Permissions = @() + $parameters.property.isInherited = $false + } + + It "Should not throw any exceptions" { + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + # Set up the parameters for the DSC resource invocation. + $parameters.Method = 'Test' + $result = Invoke-DscResource @parameters + $result.InDesiredState | Should -BeTrue + } + } + +} diff --git a/tests/Integration/Resources/AzDoGitRepository.tests.ps1 b/tests/Integration/Resources/AzDoGitRepository.tests.ps1 new file mode 100644 index 000000000..574e932bb --- /dev/null +++ b/tests/Integration/Resources/AzDoGitRepository.tests.ps1 @@ -0,0 +1,127 @@ +Describe "AzDoGitRepository Integration Tests" { + + BeforeAll { + + # Perform setup tasks here + $PROJECTNAME = 'TESTPROJECT_GITREPOSITORY' + + # Define common parameters + $parameters = @{ + Name = 'AzDoGitRepository' + ModuleName = 'AzureDevOpsDsc' + } + + # + # Create a new project + + New-Project $PROJECTNAME + + } + + # This context is used to test if a git repository exists. + Context "Testing if a Git Repository Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + RepositoryName = 'TESTREPOSITORY' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return False" { + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Absent', + # indicating that the git repository 'TESTREPOSITORY' does not exist. + $result.InDesiredState | Should -BeFalse + } + + } + + # This context is used to test the creation of a new git repository. + Context "Creating a new Git Repository Permissions" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + RepositoryName = 'TESTREPOSITORY' + Ensure = 'Present' + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + # Update the 'Method' property to 'Test' to test the presence of the git repository. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the git repository 'TESTREPOSITORY' exists. + $result.InDesiredState | Should -BeTrue + } + + } + + # This context is used to test the deletion of a git repository. + Context "Deleting an Existing Git Repository" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are deleting an existing resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + RepositoryName = 'TESTREPOSITORY' + Ensure = 'Absent' + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Absent', + # indicating that the git repository 'TESTREPOSITORY' was deleted. + $result.InDesiredState | Should -BeTrue + } + + } + +} diff --git a/tests/Integration/Resources/AzDoGroupMember.tests.ps1 b/tests/Integration/Resources/AzDoGroupMember.tests.ps1 new file mode 100644 index 000000000..e10395dc2 --- /dev/null +++ b/tests/Integration/Resources/AzDoGroupMember.tests.ps1 @@ -0,0 +1,133 @@ +Describe "AzDoGroupMember Integration Tests" { + + BeforeAll { + + # Perform setup tasks here + $PROJECTNAME = 'TESTPROJECT_GROUPMEMBER' + + # Define common parameters + $parameters = @{ + Name = 'AzDoGroupMember' + ModuleName = 'AzureDevOpsDsc' + } + + # + # Create a new project + + New-Project $PROJECTNAME + + # + # Create some new groups + + 'TESTGROUP' ,'Group1', 'Group2' | ForEach-Object { + New-Group -ProjectName $PROJECTNAME -GroupName $_ + } + + } + + # This context is used to test if a group member exists. + Context "Testing if a Group Member Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT_GROUPMEMBER'. + $parameters.property = @{ + GroupName = '[{0}]\TESTGROUP' -f $PROJECTNAME + GroupMembers = "[$PROJECTNAME]\Group1", "[$PROJECTNAME]\Group2" + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return False" { + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Absent', + # indicating that the group member 'TESTMEMBER' does not exist. + $result.InDesiredState | Should -BeFalse + } + + + } + + # This context is used to test the creation of a new group member. + Context "Creating a new Group Member" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + $parameters.property = @{ + GroupName = "$PROJECTNAME\TESTGROUP" + GroupMembers = "[$PROJECTNAME]\Group1", "[$PROJECTNAME]\Group2" + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + # Set the 'Method' to 'Test' to test the presence of the group member. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the group member 'TESTMEMBER' exists. + $result.InDesiredState | Should -BeTrue + } + + } + + # This context is used to test the removal of a group member. + Context "Removing a Group Member" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are removing a resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + $parameters.property = @{ + GroupName = "$PROJECTNAME\TESTGROUP" + GroupMembers = @("[$PROJECTNAME]\Group1") + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + # Set the 'Method' to 'Test' to test the presence of the group member. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Absent', + # indicating that the group member 'TESTMEMBER' does not exist. + $result.InDesiredState | Should -BeTrue + } + + } + +} diff --git a/tests/Integration/Resources/AzDoGroupPermission.tests.ps1 b/tests/Integration/Resources/AzDoGroupPermission.tests.ps1 new file mode 100644 index 000000000..a8204235d --- /dev/null +++ b/tests/Integration/Resources/AzDoGroupPermission.tests.ps1 @@ -0,0 +1,130 @@ +Describe "AzDoGroupPermission intergration tests" -skip { + + BeforeAll { + + # Perform setup tasks here + $PROJECTNAME = 'TESTPROJECT_GIT_GROUP_PERMISSION' + $GroupName = 'TESTGROUP' + + $parameters = @{ + Name = 'AzDoGroupPermission' + ModuleName = 'AzureDevOpsDsc' + property = @{ + GroupName = "[$PROJECTNAME]\$GroupName" + isInherited = $false + Permissions = @( + @{ + Identity = "this" + Permission = @{ + Read = 'Allow' + Write = 'Allow' + } + } + @{ + Identity = "[$PROJECTNAME]\Group1" + Permission = @{ + Read = 'Allow' + Write = 'Allow' + } + } + ) + } + } + + # + # Create a new project + + New-Project $PROJECTNAME + + # + # Create a new group + + New-Group $GroupName -ProjectName $PROJECTNAME + New-Group 'Group1' -ProjectName $PROJECTNAME + + } + + Context "Testing if the permissions exist" { + + BeforeAll { + $parameters.Method = 'Test' + } + + It "Should return False" { + $result = Invoke-DscResource @parameters + $result.InDesiredState | Should -BeFalse + } + + } + + # Create a new group + Context "Setting new Group Permissions" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + # Set the Method to 'Test' to verify that the git repository exists. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the git repository 'TESTREPOSITORY' exists. + $result.InDesiredState | Should -BeTrue + } + + } + + # Change the permissions + Context "Changing Group Permissions" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Set the permissions to a new value + $parameters.property.Permissions = @( + @{ + Identity = "[$PROJECTNAME]\Group1" + Permission = @{ + Read = 'Allow' + Write = 'Deny' + } + } + ) + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + # Set the Method to 'Test' to verify that the git repository exists. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the git repository 'TESTREPOSITORY' exists. + $result.InDesiredState | Should -BeTrue + } + + } + +} diff --git a/tests/Integration/Resources/AzDoOrganizationGroup.Description.tests.ps1 b/tests/Integration/Resources/AzDoOrganizationGroup.Description.tests.ps1 new file mode 100644 index 000000000..529976e10 --- /dev/null +++ b/tests/Integration/Resources/AzDoOrganizationGroup.Description.tests.ps1 @@ -0,0 +1,153 @@ +Describe "AzDoOrganizationGroup Integration Tests - With Description" { + + BeforeAll { + + # Perform setup tasks here + + # Define common parameters + $parameters = @{ + Name = 'AzDoOrganizationGroup' + ModuleName = 'AzureDevOpsDsc' + } + + $GROUPNAME = 'TESTORGANIZATIONGROUP' + + } + + # This context is used to test if a organization group exists. + Context "Testing if a Organization Group Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + GroupName = $GROUPNAME + GroupDescription = 'This is a test organization group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return False" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is not in the desired state + $result.InDesiredState | Should -BeFalse + } + } + + # This context is used to test if a organization group can be created. + Context "Testing if a Organization Group Can Be Created" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + GroupName = $GROUPNAME + GroupDescription = 'This is a test organization group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is in the desired state + $result.InDesiredState | Should -BeTrue + } + } + + # This context is used to test if a organization group can be updated. + Context "Testing if a Organization Group Can Be Updated" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + GroupName = $GROUPNAME + GroupDescription = 'This is an updated test organization group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is in the desired state + $result.InDesiredState | Should -BeTrue + } + } + + # This context is used to test if a organization group can be removed. + Context "Testing if a Organization Group Can Be Removed" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + Ensure = 'Absent' + GroupName = $GROUPNAME + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True (since ensure is set to Absent)" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is not in the desired state + $result.InDesiredState | Should -BeTrue + } + } + +} diff --git a/tests/Integration/Resources/AzDoOrganizationGroup.NoDescription.tests.ps1 b/tests/Integration/Resources/AzDoOrganizationGroup.NoDescription.tests.ps1 new file mode 100644 index 000000000..a6943d6f4 --- /dev/null +++ b/tests/Integration/Resources/AzDoOrganizationGroup.NoDescription.tests.ps1 @@ -0,0 +1,158 @@ +Describe "AzDoOrganizationGroup Integration Tests - No Description" { + + BeforeAll { + + # Perform setup tasks here + + # Define common parameters + $parameters = @{ + Name = 'AzDoOrganizationGroup' + ModuleName = 'AzureDevOpsDsc' + } + + $PROJECTNAME = 'TESTORGANIZATIONGROUP' + + # + # Create a new project + + New-Project $PROJECTNAME + + } + + # This context is used to test if a organization group exists. + Context "Testing if a Organization Group Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + GroupName = 'TESTORGANIZATIONGROUP' + GroupDescription = 'This is a test organization group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return False" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is not in the desired state + $result.InDesiredState | Should -BeFalse + } + } + + # This context is used to test if a organization group can be created. + Context "Testing if a Organization Group Can Be Created" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + GroupName = 'TESTORGANIZATIONGROUP' + GroupDescription = 'This is a test organization group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is in the desired state + $result.InDesiredState | Should -BeTrue + } + } + + # This context is used to test if a organization group can be updated. + Context "Testing if a Organization Group Can Be Updated" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + GroupName = 'TESTORGANIZATIONGROUP' + GroupDescription = 'This is an updated test organization group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is in the desired state + $result.InDesiredState | Should -BeTrue + } + } + + # This context is used to test if a organization group can be removed. + Context "Testing if a Organization Group Can Be Removed" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTORGANIZATIONGROUP'. + $parameters.property = @{ + Ensure = 'Absent' + GroupName = 'TESTORGANIZATIONGROUP' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True (since ensure is set to Absent)" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that it is not in the desired state + $result.InDesiredState | Should -BeTrue + } + } + +} diff --git a/tests/Integration/Resources/AzDoProject.Description.tests.ps1 b/tests/Integration/Resources/AzDoProject.Description.tests.ps1 new file mode 100644 index 000000000..2559a46e2 --- /dev/null +++ b/tests/Integration/Resources/AzDoProject.Description.tests.ps1 @@ -0,0 +1,152 @@ +Describe "AzDoProject Integration Tests - With Description" { + + BeforeAll { + + # Perform setup tasks here + + # Define common parameters + $parameters = @{ + Name = 'AzDoProject' + ModuleName = 'AzureDevOpsDsc' + } + + $PROJECTNAME = 'TESTPROJECT_DESC' + + } + + # This context is used to test if a project exists. + Context "Testing if a Project Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + ProjectDescription = 'Test Description' + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return False" { + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Absent', + # indicating that the project 'TESTPROJECT' does not exist. + $result.InDesiredState | Should -BeFalse + } + + } + + # This context is used to test the creation of a new project. + Context "Creating a new Project" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should be successful" { + # Set the 'Method' to 'Test' to verify that the project was successfully created. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the project specified by '$PROJECTNAME' was successfully created. + $result.InDesiredState | Should -BeTrue + } + + } + + # This context is used to test if the description of a project can be updated. + Context "Updating the Description of an existing Project" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are updating an existing resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + ProjectDescription = 'Updated Description' + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + # Set the 'Method' to 'Test' to verify that the project was successfully updated. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the project specified by '$PROJECTNAME' was successfully updated. + $result.InDesiredState | Should -BeTrue + } + + } + + # This context is used to test the deletion of an existing project. + Context "Deleting an existing Project" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are deleting an existing resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + Ensure = 'Absent' + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True (since ensure is set to Absent)" { + # Set the 'Method' to 'Test' to verify that the project was successfully deleted. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the it has been deleted successfully. + $result.InDesiredState | Should -BeTrue + } + + } + +} diff --git a/tests/Integration/Resources/AzDoProject.NoDescription.tests.ps1 b/tests/Integration/Resources/AzDoProject.NoDescription.tests.ps1 new file mode 100644 index 000000000..0930262c7 --- /dev/null +++ b/tests/Integration/Resources/AzDoProject.NoDescription.tests.ps1 @@ -0,0 +1,116 @@ +Describe "AzDoProject Integration Tests - No Description" { + + BeforeAll { + + # Perform setup tasks here + + # Define common parameters + $parameters = @{ + Name = 'AzDoProject' + ModuleName = 'AzureDevOpsDsc' + } + + $PROJECTNAME = 'TESTPROJECT' + + } + + # This context is used to test if a project exists. + Context "Testing if a Project Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return False" { + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Absent', + # indicating that the project 'TESTPROJECT' does not exist. + $result.InDesiredState | Should -BeFalse + } + + } + + # This context is used to test the creation of a new project. + Context "Creating a new Project" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should be successful" { + # Set the 'Method' to 'Test' to verify that the project was successfully created. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the project specified by '$PROJECTNAME' was successfully created. + $result.InDesiredState | Should -BeTrue + } + + } + + # This context is used to test the deletion of an existing project. + Context "Deleting an existing Project" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are deleting an existing resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + Ensure = 'Absent' + } + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True (since ensure is set to Absent)" { + # Set the 'Method' to 'Test' to verify that the project was successfully deleted. + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the it has been deleted successfully. + $result.InDesiredState | Should -BeTrue + } + + } + +} diff --git a/tests/Integration/Resources/AzDoProjectGroup.Description.tests.ps1 b/tests/Integration/Resources/AzDoProjectGroup.Description.tests.ps1 new file mode 100644 index 000000000..c6186d323 --- /dev/null +++ b/tests/Integration/Resources/AzDoProjectGroup.Description.tests.ps1 @@ -0,0 +1,167 @@ +Describe "AzDoProjectGroup Integration Tests - With Description" { + + BeforeAll { + + # + # Perform setup tasks here + + $PROJECTNAME = 'TESTPROJECT_PROJECTGROUP_DESC' + $GROUPNAME = 'TESTPROJECTGROUP' + + # Define common parameters + $parameters = @{ + Name = 'AzDoProjectGroup' + ModuleName = 'AzureDevOpsDsc' + } + + # + # Create a new project + + New-Project $PROJECTNAME + } + + # This context is used to test if a project group exists. + Context "Testing if a Project Group Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT_PROJECTGROUP'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + GroupDescription = 'This is a test project group.' + GroupName = $GROUPNAME + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + # It shouldn't be in the desired state because the project group doesn't exist. + It "Should return False" { + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'false' + $result.InDesiredState | Should -BeFalse + } + + } + + # This context is used to test the creation of a new project group. + Context "Creating a new Project Group" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT_PROJECTGROUP' and a group name 'TESTPROJECTGROUP'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + GroupName = $GROUPNAME + GroupDescription = 'This is a test project group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the project group 'TESTPROJECTGROUP' was created. + $result.InDesiredState | Should -BeTrue + } + + } + + # Test if the project description can be updated. + Context "Updating the Project Group Description" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are updating a resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT_PROJECTGROUP' and a group name 'TESTPROJECTGROUP'. + # We also specify a new description for the project group. + $parameters.property = @{ + ProjectName = $PROJECTNAME + GroupName = $GROUPNAME + GroupDescription = 'This is an updated test project group.' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the project group 'TESTPROJECTGROUP' was updated. + $result.InDesiredState | Should -BeTrue + } + + } + + # This context is used to test the removal of a project group. + Context "Removing a Project Group" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are removing a resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TESTPROJECT_PROJECTGROUP' and a group name 'TESTPROJECTGROUP'. + $parameters.property = @{ + ProjectName = $PROJECTNAME + GroupName = $GROUPNAME + Ensure = 'Absent' + } + + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + # Set the Method + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Absent', + # indicating that the project group 'TESTPROJECTGROUP' was removed. + $result.InDesiredState | Should -BeTrue + } + + } + +} diff --git a/tests/Integration/Resources/AzDoProjectServices.tests.ps1 b/tests/Integration/Resources/AzDoProjectServices.tests.ps1 new file mode 100644 index 000000000..a18101ec3 --- /dev/null +++ b/tests/Integration/Resources/AzDoProjectServices.tests.ps1 @@ -0,0 +1,133 @@ +Describe "AzDoProjectServices Integration Tests" { + + BeforeAll { + + Mock Write-Verbose { param($Message) } + + # + # Perform setup tasks here + + $PROJECTNAME = 'TEST_PROJECTSERVICES' + $GROUPNAME = 'TESTPROJECTGROUP' + + # Define common parameters + $parameters = @{ + Name = 'AzDoProjectServices' + ModuleName = 'AzureDevOpsDsc' + } + + # + # Create a new project + + New-Project $PROJECTNAME + + } + + # This context is used to test if a project services exist. + Context "Testing if a Project Services Exists" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Test', which means we are testing the presence of a resource. + $parameters.Method = 'Test' + + # Define properties for the DSC resource. + # In this case, we specify a project name 'TEST_PROJECTSERVICES'. + $properties = @{ + ProjectName = $PROJECTNAME + GitRepositories = 'Enabled' + WorkBoards = 'Enabled' + BuildPipelines = 'Enabled' + TestPlans = 'Enabled' + AzureArtifact = 'Enabled' + } + + $parameters.property = $properties + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should return True" { + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the project 'TEST_PROJECTSERVICES' exists. + $result.InDesiredState | Should -BeTrue + } + } + + # This context is used to test the creation of a new project services. + Context "Creating a new Project Services" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are creating a new resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $properties = @{ + ProjectName = $PROJECTNAME + GitRepositories = 'Enabled' + WorkBoards = 'Enabled' + BuildPipelines = 'Disabled' + TestPlans = 'Disabled' + AzureArtifact = 'Enabled' + } + + $parameters.property = $properties + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + } + + # This context is used to test the deletion of an existing project services. + Context "Disabling existing Project Services" { + + BeforeAll { + # Set up the parameters for the DSC resource invocation. + # 'Method' is set to 'Set', which means we are deleting an existing resource. + $parameters.Method = 'Set' + + # Define properties for the DSC resource. + # In this case, we specify a project name using the variable '$PROJECTNAME'. + $properties = @{ + ProjectName = $PROJECTNAME + GitRepositories = 'Disabled' + WorkBoards = 'Disabled' + BuildPipelines = 'Disabled' + TestPlans = 'Disabled' + AzureArtifact = 'Disabled' + } + + $parameters.property = $properties + } + + It "Should not throw any exceptions" { + # Test that invoking the DSC resource with the specified parameters does not throw any exceptions. + { Invoke-DscResource @parameters } | Should -Not -Throw + } + + It "Should be within desired state" { + + $parameters.Method = 'Test' + + # Invoke the DSC resource with the specified parameters and store the result. + $result = Invoke-DscResource @parameters + + # Verify that the 'Ensure' property in the result is 'Present', + # indicating that the project services were successfully disabled. + $result.InDesiredState | Should -BeTrue + } + + } + +} + diff --git a/tests/Integration/Supporting/API/Add-Header.ps1 b/tests/Integration/Supporting/API/Add-Header.ps1 new file mode 100644 index 000000000..733013d1f --- /dev/null +++ b/tests/Integration/Supporting/API/Add-Header.ps1 @@ -0,0 +1,41 @@ +Function Add-Header { + + # Dertimine the type of token. + + $headerValue = "" + + switch ($Global:DSCAZDO_AuthenticationToken.tokenType) + { + + # If the token is null + {$null} { + throw "[Add-Header] Error. The authentication token is null. Please ensure that the authentication token is set." + } + {$_ -eq 'PersonalAccessToken'} { + + # + # Personal Access Token + + # Add the Personal Access Token to the header + $headerValue = 'Authorization: Basic {0}' -f $Global:DSCAZDO_AuthenticationToken.Token + break + } + {$_ -eq 'ManagedIdentity'} { + + # Add the Managed Identity Token to the header + $headerValue = 'Bearer {0}' -f $Global:DSCAZDO_AuthenticationToken.Token + break + + } + default { + throw "[Add-Header] Error. The authentication token type is not supported." + } + + } + + Write-Verbose "[Add-Header] Adding Header" + + # Return the header value + return $headerValue + +} diff --git a/tests/Integration/Supporting/API/ConvertTo-Base64String.ps1 b/tests/Integration/Supporting/API/ConvertTo-Base64String.ps1 new file mode 100644 index 000000000..a7caec5b2 --- /dev/null +++ b/tests/Integration/Supporting/API/ConvertTo-Base64String.ps1 @@ -0,0 +1,14 @@ +function ConvertTo-Base64String +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [String] + $InputObject + ) + + process { + [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($InputObject)) + } +} diff --git a/tests/Integration/Supporting/API/Get-AzDevOpsApiVersion.ps1 b/tests/Integration/Supporting/API/Get-AzDevOpsApiVersion.ps1 new file mode 100644 index 000000000..afeb07a88 --- /dev/null +++ b/tests/Integration/Supporting/API/Get-AzDevOpsApiVersion.ps1 @@ -0,0 +1,34 @@ +function Get-AzDevOpsApiVersion +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Default + ) + + [string]$defaultApiVersion = '7.0-preview.1' + + [string[]]$apiVersions = @( + + #'4.0', # Not supported + #'5.0', # Not supported + #'5.1', # Not supported + '6.0', + '7.0-preview.1', + '7.1-preview.1', + '7.1-preview.4', + '7.2-preview.4' + + ) + + if ($Default) + { + $apiVersions = $apiVersions | + Where-Object { $_ -eq $defaultApiVersion} + } + + return $apiVersions +} diff --git a/tests/Integration/Supporting/API/Get-MIToken.ps1 b/tests/Integration/Supporting/API/Get-MIToken.ps1 new file mode 100644 index 000000000..2a81eb6a3 --- /dev/null +++ b/tests/Integration/Supporting/API/Get-MIToken.ps1 @@ -0,0 +1,73 @@ +Function Get-MIToken { + [CmdletBinding()] + param ( + # Organization Name + [Parameter(Mandatory = $true)] + [String] + $OrganizationName + ) + + Write-Verbose "[Get-MIToken] Getting the managed identity token for the organization $OrganizationName." + + # Obtain the access token from Azure AD using the Managed Identity + + $ManagedIdentityParams = @{ + # Define the Azure instance metadata endpoint to get the access token + Uri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=499b84ac-1321-427f-aa17-267ca6975798" + Method = 'Get' + HttpHeaders = @{ Metadata="true" } + ContentType = 'Application/json' + } + + # Dertimine if the machine is an arc machine + if ($env:IDENTITY_ENDPOINT) + { + + Write-Verbose "[Get-MIToken] The machine is an Azure Arc machine. The Uri needs to be updated to $($env:IDENTITY_ENDPOINT):" + $ManagedIdentityParams.Uri = '{0}?api-version=2020-06-01&resource=499b84ac-1321-427f-aa17-267ca6975798' -f $env:IDENTITY_ENDPOINT + $ManagedIdentityParams.AzureArcAuthentication = $true + + } + + Write-Verbose "[Get-MIToken] Invoking the Azure Instance Metadata Service to get the access token." + + # Invoke the RestAPI + try + { + $response = Invoke-APIRestMethod @ManagedIdentityParams + } + catch + { + # If there is an error it could be because it's an arc machine, and we need to use the secret file: + $wwwAuthHeader = $_.Exception.Response.Headers.WwwAuthenticate + if ($wwwAuthHeader -notmatch "Basic realm=.+") + { + Throw ('[Get-MIToken] {0}' -f $_) + } + + Write-Verbose "[Get-MIToken] Managed Identity Token Retrival Failed. Retrying with secret file." + + # Extract the secret file path from the WWW-Authenticate header + $secretFile = ($wwwAuthHeader -split "Basic realm=")[1] + # Read the secret file to get the token + $token = Get-Content -LiteralPath $secretFile -Raw + # Add the token to the headers + $ManagedIdentityParams.HttpHeaders.Authorization = "Basic $token" + + # Retry the request. Silently continue to suppress the error message, since we will handle it below. + $response = Invoke-APIRestMethod @ManagedIdentityParams -ErrorAction SilentlyContinue + } + + # Test the response + if ($null -eq $response.access_token) + { + throw "Error. Access token not returned from Azure Instance Metadata Service. Please ensure that the Azure Instance Metadata Service is available." + } + + # Return the token if the verify switch is not set + return @{ + tokenType = 'ManagedIdentity' + token = $response.access_token + } + +} diff --git a/tests/Integration/Supporting/API/Get-OperatingSystemInfo.ps1 b/tests/Integration/Supporting/API/Get-OperatingSystemInfo.ps1 new file mode 100644 index 000000000..44d5263eb --- /dev/null +++ b/tests/Integration/Supporting/API/Get-OperatingSystemInfo.ps1 @@ -0,0 +1,38 @@ +Function Get-OperatingSystemInfo +{ + $OS = @{ + Windows = $( + if ($PSVersionTable.PSVersion.Major -le 5) + { + $true + } + else + { + $IsWindows + } + ) + Linux = $( + if ($null -eq $IsLinux) + { + $false + } + else + { + $IsLinux + } + ) + MacOS = $( + if ($null -eq $IsMacOS) + { + $false + } + else + { + $IsMacOS + } + ) + } + + Write-Output $OS + +} diff --git a/tests/Integration/Supporting/API/Invoke-APIRestMethod.ps1 b/tests/Integration/Supporting/API/Invoke-APIRestMethod.ps1 new file mode 100644 index 000000000..b54f6f6f4 --- /dev/null +++ b/tests/Integration/Supporting/API/Invoke-APIRestMethod.ps1 @@ -0,0 +1,105 @@ +function Invoke-APIRestMethod +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject])] + param + ( + [Parameter(Mandatory=$true)] + [Alias('Uri')] + [System.String] + $ApiUri, + + [Parameter(Mandatory=$true)] + [ValidateSet('Get','Post','Patch','Put','Delete')] + [System.String] + [Alias('Method')] + $HttpMethod, + + [Parameter()] + [Hashtable] + [Alias('Headers','HttpRequestHeader')] + $HttpHeaders=@{}, + + [Parameter()] + [System.String] + [Alias('Body')] + $HttpBody, + + [Parameter()] + [System.String] + [Alias('ContentType')] + [ValidateSet('application/json','application/json-patch+json')] + $HttpContentType = 'application/json', + + [Parameter()] + [ValidateRange(0,5)] + [Int32] + $RetryAttempts = 5, + + [Parameter()] + [ValidateRange(250,10000)] + [Int32] + $RetryIntervalMs = 250, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), + + [Parameter()] + [Switch] + $NoAuthentication, + + [Parameter()] + [Switch] + $AzureArcAuthentication + + ) + + $invokeRestMethodParameters = @{ + Uri = $ApiUri + Method = $HttpMethod + Headers = $HttpHeaders + Body = $HttpBody + ContentType = $HttpContentType + ResponseHeadersVariable = 'responseHeaders' + } + + # Remove the 'Body' and 'ContentType' if not relevant to request + if ($HttpMethod -in $('Get','Delete')) + { + $invokeRestMethodParameters.Remove('Body') + $invokeRestMethodParameters.Remove('ContentType') + } + + if ($null -eq $HttpHeaders.Authorization) + { + $HttpHeaders.Authorization = Add-Header + } + + # + # Invoke the REST method + try + { + # Invoke the REST method. If the 'Verbose' switch is present, set it to $false. + # This is to prevent the output from being displayed in the console. + $response = Invoke-RestMethod @invokeRestMethodParameters -Verbose:$false + return $response + } + catch + { + # If AzureArcAuthentication is present, then we need to handle the error differently. + # Stop and Pass the error back to the caller. The caller will handle the error. + if ($AzureArcAuthentication.IsPresent) + { + throw $_ + } + + # Wait before the next attempt/retry + Start-Sleep -Milliseconds $RetryIntervalMs + + # Break the continuation token loop so that the next attempt can be made + break; + + } + +} diff --git a/tests/Integration/Supporting/API/New-AuthProvider.ps1 b/tests/Integration/Supporting/API/New-AuthProvider.ps1 new file mode 100644 index 000000000..2d7bed2d9 --- /dev/null +++ b/tests/Integration/Supporting/API/New-AuthProvider.ps1 @@ -0,0 +1,49 @@ +# A Stripped Down version of New-AzDoAuthenticationProvider +Function New-AuthProvider { + + [CmdletBinding(DefaultParameterSetName = 'PersonalAccessToken')] + param ( + # Organization Name + [Parameter(Mandatory = $true, ParameterSetName = 'PersonalAccessToken')] + [Parameter(Mandatory = $true, ParameterSetName = 'ManagedIdentity')] + [Alias('OrgName')] + [String] + $OrganizationName, + + # Personal Access Token + [Parameter(Mandatory = $true, ParameterSetName = 'PersonalAccessToken')] + [Alias('PAT')] + [String] + $PersonalAccessToken, + + # Use Managed Identity + [Parameter(ParameterSetName = 'ManagedIdentity')] + [Switch] + $useManagedIdentity + ) + + # Set the Global Variables + $Global:DSCAZDO_OrganizationName = $OrganizationName + $Global:DSCAZDO_AuthenticationToken = $null + + # If the parameterset is PersonalAccessToken + if ($PSCmdlet.ParameterSetName -eq 'PersonalAccessToken') + { + Write-Verbose "[New-AuthProvider] Creating a new Personal Access Token with OrganizationName $OrganizationName." + $Global:DSCAZDO_AuthenticationToken = @{ + 'token' = ':{0}' -f (ConvertTo-Base64String $PersonalAccessToken) + 'type' = 'PAT' + } + } + # If the parameterset is ManagedIdentity + elseif ($PSCmdlet.ParameterSetName -eq 'ManagedIdentity') + { + Write-Verbose "[New-AuthProvider] Creating a new Azure Managed Identity with OrganizationName $OrganizationName." + # If the Token is not Valid. Get a new Token. + $Global:DSCAZDO_AuthenticationToken = @{ + 'token' = Get-MIToken -OrganizationName $OrganizationName + 'type' = 'ManagedIdentity' + } + } + +} diff --git a/tests/Integration/Supporting/API/Test-isWindowsAdmin.ps1 b/tests/Integration/Supporting/API/Test-isWindowsAdmin.ps1 new file mode 100644 index 000000000..a9e58b338 --- /dev/null +++ b/tests/Integration/Supporting/API/Test-isWindowsAdmin.ps1 @@ -0,0 +1,10 @@ +Function Test-isWindowsAdmin +{ + + $currentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object System.Security.Principal.WindowsPrincipal($currentIdentity) + + # Check if the current user is in the Administrator role + (-not($principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))) + +} diff --git a/tests/Integration/Supporting/APICalls/List-DevOpsGroups.ps1 b/tests/Integration/Supporting/APICalls/List-DevOpsGroups.ps1 new file mode 100644 index 000000000..47b649ab9 --- /dev/null +++ b/tests/Integration/Supporting/APICalls/List-DevOpsGroups.ps1 @@ -0,0 +1,32 @@ +Function List-DevOpsGroups { + [CmdletBinding()] + [OutputType([System.Object])] + Param + ( + [Parameter(Mandatory = $true)] + [string] + $Organization, + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/groups" + Method = 'Get' + } + + # + # Invoke the Rest API to get the groups + $groups = Invoke-APIRestMethod @params + + if ($null -eq $groups.value) + { + return $null + } + + # + # Return the groups from the cache + return $groups.Value + +} diff --git a/tests/Integration/Supporting/APICalls/List-DevOpsProjects.ps1 b/tests/Integration/Supporting/APICalls/List-DevOpsProjects.ps1 new file mode 100644 index 000000000..4e064a6f0 --- /dev/null +++ b/tests/Integration/Supporting/APICalls/List-DevOpsProjects.ps1 @@ -0,0 +1,32 @@ + +function List-DevOpsProjects +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default) + ) + + $params = @{ + Uri = "https://dev.azure.com/$OrganizationName/_apis/projects" + Method = 'Get' + } + + # + # Invoke the Rest API to get the groups + $groups = Invoke-APIRestMethod @params + + if ($null -eq $groups.value) + { + return $null + } + + # + # Return the groups from the cache + return $groups.Value + +} diff --git a/tests/Integration/Supporting/APICalls/Remove-DevOpsGroup.ps1 b/tests/Integration/Supporting/APICalls/Remove-DevOpsGroup.ps1 new file mode 100644 index 000000000..6fc39666a --- /dev/null +++ b/tests/Integration/Supporting/APICalls/Remove-DevOpsGroup.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS +Removes a group from Azure DevOps. + +.DESCRIPTION +The Remove-DevOpsGroup function is used to remove a group from Azure DevOps using the Azure DevOps REST API. + +.PARAMETER ApiUri +The mandatory parameter for the API URI. + +.PARAMETER ApiVersion +The optional parameter for the API version with a default value obtained from the Get-AzDevOpsApiVersion function. + +.PARAMETER GroupDescriptor +The optional parameter for the project scope descriptor. + +.OUTPUTS +System.Management.Automation.PSObject + +.EXAMPLE +Remove-DevOpsGroup -ApiUri "https://dev.azure.com/myorganization" -GroupDescriptor "MyGroup" + +This example removes the group with the specified group descriptor from Azure DevOps. + +#> + +Function Remove-DevOpsGroup { + [CmdletBinding()] + [OutputType([System.Management.Automation.PSObject])] + param + ( + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion -Default), + + [Parameter(Mandatory = $true)] + [String] + $OrganizationName, + + [Parameter()] + [String] + $GroupDescriptor + ) + + $params = @{ + Uri = 'https://vssps.dev.azure.com/{0}/_apis/graph/groups/{1}?api-version={2}' -f $OrganizationName, $GroupDescriptor, $ApiVersion + Method = 'Delete' + ContentType = 'application/json' + } + + try { + return (Invoke-APIRestMethod @params) + } + catch {} + +} diff --git a/tests/Integration/Supporting/APICalls/Remove-DevOpsProject.ps1 b/tests/Integration/Supporting/APICalls/Remove-DevOpsProject.ps1 new file mode 100644 index 000000000..029218014 --- /dev/null +++ b/tests/Integration/Supporting/APICalls/Remove-DevOpsProject.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS +Removes an Azure DevOps project. + +.DESCRIPTION +The Remove-DevOpsProject function is used to remove an Azure DevOps project from the specified organization. + +.PARAMETER Organization +The name or URL of the Azure DevOps organization. + +.PARAMETER ProjectId +The ID or name of the project to be removed. + +.EXAMPLE +Remove-DevOpsProject -Organization "MyOrganization" -ProjectId "MyProject" + +This example removes the Azure DevOps project with the ID "MyProject" from the organization "MyOrganization". + +#> + +function Remove-DevOpsProject +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Organization, + + [Parameter(Mandatory = $true)] + [Alias('Name')] + [System.String] + $ProjectId, + + [Parameter()] + [String] + $ApiVersion = $(Get-AzDevOpsApiVersion | Select-Object -Last 1) + + ) + + Write-Verbose "[Remove-DevOpsProject] Started." + + # Define the API version to use + $params = @{ + Uri = 'https://dev.azure.com/{0}/_apis/projects/{1}?api-version={2}' -f $Organization, $ProjectId, $ApiVersion + Method = "DELETE" + } + + Write-Verbose "[Remove-DevOpsProject] Removing project $ProjectId from Azure DevOps organization $Organization" + + try + { + # Invoke the Azure DevOps REST API to create the project + return (Invoke-APIRestMethod @params) + + } catch + { + Write-Error "[Remove-DevOpsProject] Failed to create the Azure DevOps project: $_" + } + +} diff --git a/tests/Integration/Supporting/Functions/SupportingFunctions.ps1 b/tests/Integration/Supporting/Functions/SupportingFunctions.ps1 new file mode 100644 index 000000000..083092867 --- /dev/null +++ b/tests/Integration/Supporting/Functions/SupportingFunctions.ps1 @@ -0,0 +1,69 @@ +Function New-Project { + param( + [string]$ProjectName + ) + + # + # Create a new project + + $projectParams = @{ + Name = 'AzDoProject' + ModuleName = 'AzureDevOpsDsc' + Method = 'Set' + property = @{ + ProjectName = $PROJECTNAME + } + } + + # Invoke the DSC resource to create a new project. + $null = Invoke-DscResource @projectParams + +} + +Function New-Repository { + param( + [string]$ProjectName, + [string]$RepositoryName + ) + + # + # Create a new repository + + $parameters = @{ + Name = 'AzDoGitRepository' + ModuleName = 'AzureDevOpsDsc' + Method = 'Set' + property = @{ + ProjectName = $PROJECTNAME + RepositoryName = $RepositoryName + } + } + + # Invoke the DSC resource to create a new project. + $null = Invoke-DscResource @parameters + +} + +Function New-Group { + param( + [string]$ProjectName, + [string]$GroupName + ) + + # + # Create a new group + + $groupParams = @{ + Name = 'AzDoProjectGroup' + ModuleName = 'AzureDevOpsDsc' + Method = 'Set' + property = @{ + ProjectName = $PROJECTNAME + GroupName = $GroupName + } + } + + # Create that group + $null = Invoke-DscResource @groupParams + +} diff --git a/tests/Integration/Supporting/Initalize-TestFramework.ps1 b/tests/Integration/Supporting/Initalize-TestFramework.ps1 new file mode 100644 index 000000000..695d37814 --- /dev/null +++ b/tests/Integration/Supporting/Initalize-TestFramework.ps1 @@ -0,0 +1,45 @@ +param( + [Parameter(Mandatory = $true)] + [String]$TestFrameworkConfigurationPath +) + +Import-Module DscResource.Common.psd1 +Import-Module AzureDevOpsDsc.Common.psd1 +Import-Module AzureDevOpsDsc.psd1 + +# +# Test Framework Configuration +if (-not (Test-Path $TestFrameworkConfigurationPath)) +{ + throw "[Initialize-TestFramework] Test Framework Configuration file not found at $TestFrameworkConfigurationPath" +} + +# +# Attempt to load the configuration +$TestFrameworkConfiguration = Get-Content $TestFrameworkConfigurationPath | ConvertFrom-Json + +# Confirm the Organization +if (-not $TestFrameworkConfiguration.Organization) +{ + throw "[Initialize-TestFramework] Organization not specified in the Test Framework Configuration" +} + +# Confirm the Authentication Type +if ($TestFrameworkConfiguration.AuthenticationType -eq 'PAT') +{ + # Authenticate with a Personal Access Token + #New-AuthProvider -OrganizationName $TestFrameworkConfiguration.Organization -PersonalAccessToken $TestFrameworkConfiguration.PATToken + New-AzDoAuthenticationProvider -OrganizationName $TestFrameworkConfiguration.Organization -PersonalAccessToken $TestFrameworkConfiguration.PATToken +} +elseif ($TestFrameworkConfiguration.AuthenticationType -eq 'ManagedIdentity') +{ + # Authenticate with a Managed Identity + #New-AuthProvider -OrganizationName $TestFrameworkConfiguration.Organization -useManagedIdentity + New-AzDoAuthenticationProvider -OrganizationName $TestFrameworkConfiguration.Organization -useManagedIdentity +} +else +{ + throw "[Initialize-TestFramework] Invalid Authentication Type: $($TestFrameworkConfiguration.AuthenticationType)" +} + + diff --git a/tests/Integration/Supporting/Teardown.ps1 b/tests/Integration/Supporting/Teardown.ps1 new file mode 100644 index 000000000..220fe5203 --- /dev/null +++ b/tests/Integration/Supporting/Teardown.ps1 @@ -0,0 +1,48 @@ +[CmdletBinding()] +param ( + [Parameter()] + [Switch] + $ClearAll, + + [Parameter()] + [Switch] + $ClearOrganizationGroups, + + [Parameter()] + [Switch] + $ClearProjects, + + [Parameter()] + [String] + $OrganizationName, + + [Parameter()] + [Object] + $TestFrameworkConfiguration + +) + +$Global:DSCAZDO_AuthenticationToken = Get-MIToken -OrganizationName $OrganizationName + +# +# Remove Projects +if ($ClearAll -or $ClearProjects) +{ + # List all projects and remove them + List-DevOpsProjects -OrganizationName $OrganizationName | Where-Object { $_.Name -notin $TestFrameworkConfiguration.excludedProjectsFromTeardown } | ForEach-Object { + Remove-DevOpsProject -ProjectId $_.id -Organization $OrganizationName + } +} + +# +# Remove Organization Groups + +if ($ClearAll -or $ClearOrganizationGroups) +{ + # List all groups and remove them + List-DevOpsGroups -Organization $OrganizationName | Where-Object { + ($_.DisplayName -notlike "Project*") -and ($_.DisplayName -notlike "Security*") -and ($_.DisplayName -notlike "Service*") -and ($_.DisplayName -notlike "Team*") -and ($_.DisplayName -notlike "Enterprise*") + } | ForEach-Object { + Remove-DevOpsGroup -GroupDescriptor $_.descriptor -OrganizationName $OrganizationName + } +} diff --git a/tests/Integration/TestFrameworkConfiguration.json b/tests/Integration/TestFrameworkConfiguration.json new file mode 100644 index 000000000..5afce3ea9 --- /dev/null +++ b/tests/Integration/TestFrameworkConfiguration.json @@ -0,0 +1,7 @@ +{ + "Organization" : "akkodistestorg", + "AuthenticationType" : "ManagedIdentity", + "excludedProjectsFromTeardown": [ + "DSC Pipeline Services" + ] +} diff --git a/tests/Unit/Classes/API/007.APIRateLimit.tests.ps1 b/tests/Unit/Classes/API/007.APIRateLimit.tests.ps1 new file mode 100644 index 000000000..d3e380eaf --- /dev/null +++ b/tests/Unit/Classes/API/007.APIRateLimit.tests.ps1 @@ -0,0 +1,76 @@ +# Requires -Module Pester -Version 5.0.0 +# Requires -Module DscResource.Common + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'APIRateLimit' { + + BeforeAll { + Mock Write-Warning + } + + Context 'Constructor with HashTable parameter' { + It 'should initialize properties correctly when given a valid hashtable' { + $validHashTable = @{ + 'Retry-After' = 10 + 'X-RateLimit-Remaining' = 100 + 'X-RateLimit-Reset' = 1609459200 # Unix time for 2021-01-01 00:00:00 UTC + } + + $apiRateLimit = [APIRateLimit]::new($validHashTable) + + $apiRateLimit.retryAfter | Should -Be 10 + $apiRateLimit.XRateLimitRemaining | Should -Be 100 + $apiRateLimit.XRateLimitReset | Should -Be 1609459200 + } + + It 'should throw an error when given an invalid hashtable' { + $invalidHashTable = @{ + 'Retry-After' = 10 + 'X-RateLimit-Remaining' = 100 + # Missing 'X-RateLimit-Reset' + } + + { [APIRateLimit]::new($invalidHashTable) } | Should -Throw "The APIRateLimitObj is not valid." + } + } + + Context 'Constructor with retryAfter parameter' { + It 'should initialize retryAfter property correctly' { + $retryAfterValue = 5 + $apiRateLimit = [APIRateLimit]::new($retryAfterValue) + + $apiRateLimit.retryAfter | Should -Be $retryAfterValue + } + } + + Context 'isValid method' { + It 'should return true for a valid hashtable' { + $validHashTable = @{ + 'Retry-After' = 10 + 'X-RateLimit-Remaining' = 100 + 'X-RateLimit-Reset' = 1609459200 + } + $apiRateLimit = [APIRateLimit]::new($validHashTable) + $result = $apiRateLimit.isValid($validHashTable) + + $result | Should -Be $true + } + + It 'should return false for an invalid hashtable' { + $invalidHashTable = @{ + 'Retry-After' = 10 + 'X-RateLimit-Remaining' = 100 + # Missing 'X-RateLimit-Reset' + } + {[APIRateLimit]::new($invalidHashTable)} | Should -Throw '*The APIRateLimitObj is not valid*' + } + } +} diff --git a/tests/Unit/Classes/Authentication/001.AuthenticationToken.tests.ps1 b/tests/Unit/Classes/Authentication/001.AuthenticationToken.tests.ps1 new file mode 100644 index 000000000..a174aec1a --- /dev/null +++ b/tests/Unit/Classes/Authentication/001.AuthenticationToken.tests.ps1 @@ -0,0 +1,90 @@ +# Requires -Module Pester -Version 5.0.0 + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'AuthenticationToken Class' { + Context 'ConvertFromSecureString Method' { + It 'Should convert a SecureString to a String correctly' { + # Arrange + $secureString = ConvertTo-SecureString "TestPassword" -AsPlainText -Force + $authToken = [AuthenticationToken]::new() + + # Act + $result = $authToken.ConvertFromSecureString($secureString) + + # Assert + $result | Should -Be "TestPassword" + } + } + + Context 'TestCallStack Method' { + It 'Should return true if the calling function is found in the call stack' { + # Arrange + function Test-CallStackFunction + { + $authToken = [AuthenticationToken]::new() + return $authToken.TestCallStack('Test-CallStackFunction') + } + + # Act + $result = Test-CallStackFunction + + # Assert + $result | Should -Be $true + } + + It 'Should return false if the calling function is not found in the call stack' { + # Arrange + $authToken = [AuthenticationToken]::new() + + # Act + $result = $authToken.TestCallStack('NonExistentFunction') + + # Assert + $result | Should -Be $false + } + } + + Context 'TestCaller Method' { + It 'Should throw an exception if called from an unauthorized function' { + # Arrange + $authToken = [AuthenticationToken]::new() + + # Act & Assert + { $authToken.TestCaller() } | Should -Throw "*The Get() method can only be called*" + } + } + + Context 'Get Method' { + It 'Should return the access token when called from an authorized function' { + # Arrange + function Invoke-AzDevOpsApiRestMethod + { + $authToken = [AuthenticationToken]::new() + $authToken.access_token = ConvertTo-SecureString "TestToken" -AsPlainText -Force + return $authToken.Get() + } + + # Act + $result = Invoke-AzDevOpsApiRestMethod + + # Assert + $result | Should -Be "TestToken" + } + + It 'Should throw an exception when called from an unauthorized function' { + # Arrange + $authToken = [AuthenticationToken]::new() + + # Act & Assert + { $authToken.Get() } | Should -Throw "*The Get() method can only be called*" + } + } +} diff --git a/tests/Unit/Classes/Authentication/002.PersonalAccessToken.tests.ps1 b/tests/Unit/Classes/Authentication/002.PersonalAccessToken.tests.ps1 new file mode 100644 index 000000000..703e4aecb --- /dev/null +++ b/tests/Unit/Classes/Authentication/002.PersonalAccessToken.tests.ps1 @@ -0,0 +1,84 @@ +# Requires -Module Pester -Version 5.0.0 + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'PersonalAccessToken Class' { + Context 'Constructor with String Parameter' { + It 'Should initialize with a string personal access token' { + # Arrange + $personalAccessToken = "TestToken" + + # Act + $pat = [PersonalAccessToken]::new($personalAccessToken) + + # Assert + $pat.tokenType | Should -Be 'PersonalAccessToken' + $pat.ConvertFromSecureString($pat.access_token) | Should -Be "OlRlc3RUb2tlbg==" + } + } + + Context 'Constructor with SecureString Parameter' { + It 'Should initialize with a secure string personal access token' { + # Arrange + $secureStringPAT = ConvertTo-SecureString "TestSecureToken" -AsPlainText -Force + + # Act + $pat = [PersonalAccessToken]::new($secureStringPAT) + + # Assert + $pat.tokenType | Should -Be 'PersonalAccessToken' + $pat.access_token | Should -Be $secureStringPAT + } + } + + Context 'isExpired Method' { + It 'Should always return false' { + # Arrange + $pat = [PersonalAccessToken]::new("TestToken") + + # Act + $result = $pat.isExpired() + + # Assert + $result | Should -Be $false + } + } +} + +Describe 'New-PersonalAccessToken Function' { + It 'Should create a new PersonalAccessToken object with a string token' { + # Arrange + $personalAccessToken = "TestToken" + + # Act + $pat = New-PersonalAccessToken -PersonalAccessToken $personalAccessToken + + # Assert + $pat | Should -BeOfType [PersonalAccessToken] + $pat.ConvertFromSecureString($pat.access_token) | Should -Be "OlRlc3RUb2tlbg==" + } + + It 'Should create a new PersonalAccessToken object with a secure string token' { + # Arrange + $secureStringPAT = ConvertTo-SecureString "TestSecureToken" -AsPlainText -Force + + # Act + $pat = New-PersonalAccessToken -SecureStringPersonalAccessToken $secureStringPAT + + # Assert + $pat | Should -BeOfType [PersonalAccessToken] + $pat.access_token | Should -Be $secureStringPAT + } + + It 'Should throw an error if no token is provided' { + # Act & Assert + { New-PersonalAccessToken } | Should -Throw "Error. A Personal Access Token or SecureString Personal Access Token must be provided." + } +} diff --git a/tests/Unit/Classes/Authentication/003.ManagedIdentityToken.tests.ps1 b/tests/Unit/Classes/Authentication/003.ManagedIdentityToken.tests.ps1 new file mode 100644 index 000000000..48164ebfd --- /dev/null +++ b/tests/Unit/Classes/Authentication/003.ManagedIdentityToken.tests.ps1 @@ -0,0 +1,144 @@ +# Requires -Module Pester -Version 5.0.0 + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + + +Describe 'ManagedIdentityToken Class' { + Context 'Constructor with PSCustomObject Parameter' { + + BeforeAll { + $epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) + } + + It 'Should initialize with a valid ManagedIdentityTokenObj' { + # Arrange + + $managedIdentityTokenObj = [PSCustomObject]@{ + access_token = "TestAccessToken" + expires_on = ($epochStart.AddMinutes(10) - [datetime]::UnixEpoch).TotalSeconds + expires_in = 600 + resource = "https://resource.url" + token_type = "Bearer" + } + + # Act + $mit = [ManagedIdentityToken]::new($managedIdentityTokenObj) + + # Assert + $mit.tokenType | Should -Be 'ManagedIdentity' + $mit.expires_on | Should -Be $epochStart.AddMinutes(10) + $mit.expires_in | Should -Be 600 + $mit.resource | Should -Be "https://resource.url" + $mit.token_type | Should -Be "Bearer" + } + + It 'Should throw an error with an invalid ManagedIdentityTokenObj' { + # Arrange + $invalidManagedIdentityTokenObj = [PSCustomObject]@{ + access_token = "TestAccessToken" + expires_on = ($epochStart.AddMinutes(10) - [datetime]::UnixEpoch).TotalSeconds + expires_in = 600 + resource = "https://resource.url" + # Missing token_type + } + + # Act & Assert + { [ManagedIdentityToken]::new($invalidManagedIdentityTokenObj) } | Should -Throw '*The ManagedIdentityTokenObj is not valid*' + } + } + + Context 'isExpired Method' { + + BeforeAll { + $epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) + } + + It 'Should return true if the token is expired' { + # Arrange + $expiredTokenObj = [PSCustomObject]@{ + access_token = "TestAccessToken" + expires_on = ($epochStart.AddMinutes(-10) - [datetime]::UnixEpoch).TotalSeconds + expires_in = -600 + resource = "https://resource.url" + token_type = "Bearer" + } + $mit = [ManagedIdentityToken]::new($expiredTokenObj) + + # Act + $result = $mit.isExpired() + + # Assert + $result | Should -Be $true + } + + It 'Should return false if the token is not expired' { + # Arrange + $validTokenObj = [PSCustomObject]@{ + access_token = "TestAccessToken" + expires_on = 1820701735 + expires_in = 600 + resource = "https://resource.url" + token_type = "Bearer" + } + $mit = [ManagedIdentityToken]::new($validTokenObj) + + # Act + $result = $mit.isExpired() + + # Assert + $result | Should -Be $false + } + } + +} + +Describe 'New-ManagedIdentityToken Function' { + + BeforeAll { + $epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) + } + + It 'Should create a new ManagedIdentityToken object with a valid PSCustomObject' { + # Arrange + $epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) + $managedIdentityTokenObj = [PSCustomObject]@{ + access_token = "TestAccessToken" + expires_on = ($epochStart.AddMinutes(10) - [datetime]::UnixEpoch).TotalSeconds + expires_in = 600 + resource = "https://resource.url" + token_type = "Bearer" + } + + # Act + $mit = New-ManagedIdentityToken -ManagedIdentityTokenObj $managedIdentityTokenObj + + # Assert + $mit | Should -BeOfType [ManagedIdentityToken] + $mit.ConvertFromSecureString($mit.access_token) | Should -Be "TestAccessToken" + $mit.expires_on | Should -Be $epochStart.AddMinutes(10) + $mit.expires_in | Should -Be 600 + $mit.resource | Should -Be "https://resource.url" + $mit.token_type | Should -Be "Bearer" + } + + It 'Should throw an error if the ManagedIdentityTokenObj is invalid' { + # Arrange + $invalidManagedIdentityTokenObj = [PSCustomObject]@{ + access_token = "TestAccessToken" + expires_on = ($epochStart.AddMinutes(10) - [datetime]::UnixEpoch).TotalSeconds + expires_in = 600 + resource = "https://resource.url" + # Missing token_type + } + + # Act & Assert + { New-ManagedIdentityToken -ManagedIdentityTokenObj $invalidManagedIdentityTokenObj } | Should -Throw "*The ManagedIdentityTokenObj is not valid*" + } +} diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/AzDevOpsApiDscResourceBase.Initialization.Tests.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/AzDevOpsApiDscResourceBase.Initialization.Tests.ps1 deleted file mode 100644 index b9e289e79..000000000 --- a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/AzDevOpsApiDscResourceBase.Initialization.Tests.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -# Initialize tests for module function 'Classes' -. $PSScriptRoot\..\Classes.TestInitialization.ps1 - -# Note: Use of this functionality seems to pre-load the module and classes which subsquent tests can use -# which works around difficulty of referencing classes in 'source' directory when code coverage is -# using the dynamically/build-defined, 'output' directory. -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - -} diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/BeforeAll.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/BeforeAll.ps1 new file mode 100644 index 000000000..0dc71e37c --- /dev/null +++ b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/BeforeAll.ps1 @@ -0,0 +1,8 @@ +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceFunctionName.Tests.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceFunctionName.Tests.ps1 index 3c7ea0e61..570fde031 100644 --- a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceFunctionName.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceFunctionName.Tests.ps1 @@ -1,126 +1,110 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 - -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 - -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) - - - Describe "$script:subModuleName\Classes\AzDevOpsApiDscResourceBase\$script:commandName" -Tag $script:tag { - - $testCasesValidRequiredActionWithFunctions = @( - @{ - RequiredAction = 'Get' - }, - @{ - RequiredAction = 'New' - }, - @{ - RequiredAction = 'Set' - }, - @{ - RequiredAction = 'Remove' - }, - @{ - RequiredAction = 'Test' - } - ) - - $testCasesValidRequiredActionWithoutFunctions = @( - @{ - RequiredAction = 'Error' - }, - @{ - RequiredAction = 'None' - } - ) - - $testCasesInvalidRequiredActions = @( - @{ - RequiredAction = 'SomethingInvalid' - }, - @{ - RequiredAction = $null - }, - @{ - RequiredAction = '' - } - ) +Describe "[AzDevOpsApiDscResourceBase]::GetResourceFunctionName() Tests" -Tag 'Unit', 'AzDevOpsApiDscResourceBase' { + + $testCasesValidRequiredActionWithFunctions = @( + @{ + RequiredAction = 'Get' + }, + @{ + RequiredAction = 'New' + }, + @{ + RequiredAction = 'Set' + }, + @{ + RequiredAction = 'Remove' + }, + @{ + RequiredAction = 'Test' + } + ) + + $testCasesValidRequiredActionWithoutFunctions = @( + @{ + RequiredAction = 'Error' + }, + @{ + RequiredAction = 'None' + } + ) + + $testCasesInvalidRequiredActions = @( + @{ + RequiredAction = 'SomethingInvalid' + }, + @{ + RequiredAction = $null + }, + @{ + RequiredAction = '' + } + ) - class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) - { - [DscProperty(Key)] - [string]$DscKey - [string]GetResourceName() - { - return 'ApiDscResourceBaseExample' - } + class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$DscKey + + [string]GetResourceName() + { + return 'ApiDscResourceBaseExample' } + } - Context 'When called with valid "RequiredAction" values' { + Context 'When called with valid "RequiredAction" values' { - Context 'When "RequiredAction" value should have a related function' { + Context 'When "RequiredAction" value should have a related function' { - It 'Should not throw - ' -TestCases $testCasesValidRequiredActionWithFunctions { - param ([System.String]$RequiredAction) + It 'Should not throw - ' -TestCases $testCasesValidRequiredActionWithFunctions { + param ([System.String]$RequiredAction) - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - {$azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction)} | Should -Not -Throw - } + {$azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction)} | Should -Not -Throw + } - It 'Should return the correct, function name - ""' -TestCases $testCasesValidRequiredActionWithFunctions { - param ([System.String]$RequiredAction) + It 'Should return the correct, function name - ""' -TestCases $testCasesValidRequiredActionWithFunctions { + param ([System.String]$RequiredAction) - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - $azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction) | Should -Be "$($RequiredAction)-AzDevOpsApiDscResourceBaseExample" - } + $azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction) | Should -Be "$($RequiredAction)-ApiDscResourceBaseExample" } + } - Context 'When "RequiredAction" value should not have a related function' { + Context 'When "RequiredAction" value should not have a related function' { - It 'Should not throw - ' -TestCases $testCasesValidRequiredActionWithoutFunctions { - param ([System.String]$RequiredAction) + It 'Should not throw - ' -TestCases $testCasesValidRequiredActionWithoutFunctions { + param ([System.String]$RequiredAction) - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - {$azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction)} | Should -Not -Throw - } + {$azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction)} | Should -Not -Throw + } - It 'Should return the correct, function name - ""' -TestCases $testCasesValidRequiredActionWithoutFunctions { - param ([System.String]$RequiredAction) + It 'Should return the correct, function name - ""' -TestCases $testCasesValidRequiredActionWithoutFunctions { + param ([System.String]$RequiredAction) - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - $azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction) | Should -BeNullOrEmpty - } + $azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction) | Should -BeNullOrEmpty } } + } - Context 'When called with invalid "RequiredAction" values' { + Context 'When called with invalid "RequiredAction" values' { - It 'Should not throw - ' -TestCases $testCasesInvalidRequiredActions { - param ([System.String]$RequiredAction) + It 'Should not throw - ' -TestCases $testCasesInvalidRequiredActions { + param ([System.String]$RequiredAction) - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - {$azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction)} | Should -Throw - } + {$azDevOpsApiDscResourceBase.GetResourceFunctionName($RequiredAction)} | Should -Throw } } } + diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceId.Tests.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceId.Tests.ps1 index 0abb19f63..0b0fc6348 100644 --- a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceId.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceId.Tests.ps1 @@ -1,51 +1,34 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsApiDscResourceBase]::GetResourceId() Tests" -Tag 'Unit', 'AzDevOpsApiDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { + class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$DscKey - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) - - - Describe "$script:subModuleName\Classes\AzDevOpsApiDscResourceBase\$script:commandName" -Tag $script:tag { - - - class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [DscProperty(Key)] - [string]$DscKey - - [string]GetResourceName() - { - return 'ApiDscResourceBaseExample' - } + return 'ApiDscResourceBaseExample' } + } - Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { + Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { - It 'Should not throw' { + It 'Should not throw' { - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty - {$azDevOpsApiDscResourceBase.GetResourceId()} | Should -Not -Throw - } + {$azDevOpsApiDscResourceBase.GetResourceId()} | Should -Not -Throw + } - It 'Should return the same name as the DSC Resource/class without the expected prefix' { + It 'Should return the same name as the DSC Resource/class without the expected prefix' { - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty - $azDevOpsApiDscResourceBase.GetResourceId() | Should -Be 'SomeIdValue' - } + $azDevOpsApiDscResourceBase.GetResourceId() | Should -Be 'SomeIdValue' } } } + diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceIdPropertyName.Tests.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceIdPropertyName.Tests.ps1 index 320b3a6e7..b7c7f5beb 100644 --- a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceIdPropertyName.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceIdPropertyName.Tests.ps1 @@ -1,51 +1,35 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsApiDscResourceBase]::GetResourceIdPropertyName() tests" -Tag 'Unit', 'AzDevOpsApiDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { + class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$DscKey - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) - - - Describe "$script:subModuleName\Classes\AzDevOpsApiDscResourceBase\$script:commandName" -Tag $script:tag { - - class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [DscProperty(Key)] - [string]$DscKey - - [string]GetResourceName() - { - return 'ApiDscResourceBaseExample' - } + return 'ApiDscResourceBaseExample' } + } - Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { + Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { - It 'Should not throw' { + It 'Should not throw' { - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty - {$azDevOpsApiDscResourceBase.GetResourceIdPropertyName()} | Should -Not -Throw - } + {$azDevOpsApiDscResourceBase.GetResourceIdPropertyName()} | Should -Not -Throw + } - It 'Should return the same name as the DSC Resource/class without the expected prefix' { + It 'Should return the same name as the DSC Resource/class without the expected prefix' { - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase | Add-Member -Name 'ApiDscResourceBaseExampleId' -Value 'SomeIdValue' -MemberType NoteProperty - $azDevOpsApiDscResourceBase.GetResourceIdPropertyName() | Should -Be 'ApiDscResourceBaseExampleId' - } + $azDevOpsApiDscResourceBase.GetResourceIdPropertyName() | Should -Be 'ApiDscResourceBaseExampleId' } } } + diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKey.Tests.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKey.Tests.ps1 index e6b883b98..09134e483 100644 --- a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKey.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKey.Tests.ps1 @@ -1,52 +1,36 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsApiDscResourceBase]::GetResourceKey() Tests" -Tag 'Unit', 'AzDevOpsApiDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { + class AzDevOpsApiDscResourceBaseWithKey : AzDevOpsApiDscResourceBase + { + [System.String]$ApiDscResourceBaseId - Describe "$script:subModuleName\Classes\AzDevOpsApiDscResourceBase\$script:commandName" -Tag $script:tag { - - - Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { + [DscProperty(Key)] + [System.String]$ApiDscResourceBaseKey + } - class AzDevOpsApiDscResourceBaseWithKey : AzDevOpsApiDscResourceBase - { - [System.String]$ApiDscResourceBaseId + It 'Should not throw' { - [DscProperty(Key)] - [System.String]$ApiDscResourceBaseKey + $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ + ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' + ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' } - It 'Should not throw' { + {$azDevOpsApiDscResourceBaseWithKey.GetResourceKey()} | Should -Not -Throw + } - $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ - ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' - ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' - } + It 'Should return the same name as the DSC Resource/class without the expected prefix' { - {$azDevOpsApiDscResourceBaseWithKey.GetResourceKey()} | Should -Not -Throw + $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ + ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' + ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' } - It 'Should return the same name as the DSC Resource/class without the expected prefix' { - - $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ - ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' - ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' - } - - $azDevOpsApiDscResourceBaseWithKey.GetResourceKey() | Should -Be 'ApiDscResourceBaseKeyValue' - } + $azDevOpsApiDscResourceBaseWithKey.GetResourceKey() | Should -Be 'ApiDscResourceBaseKeyValue' } } } + diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKeyPropertyName.Tests.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKeyPropertyName.Tests.ps1 index 06117a6be..743837832 100644 --- a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKeyPropertyName.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceKeyPropertyName.Tests.ps1 @@ -1,52 +1,36 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsApiDscResourceBase]::GetResourceKeyPropertyName() tests" -Tag 'Unit', 'AzDevOpsApiDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { + class AzDevOpsApiDscResourceBaseWithKey : AzDevOpsApiDscResourceBase + { + [System.String]$ApiDscResourceBaseId - Describe "$script:subModuleName\Classes\AzDevOpsApiDscResourceBase\$script:commandName" -Tag $script:tag { - - - Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { + [DscProperty(Key)] + [System.String]$ApiDscResourceBaseKey + } - class AzDevOpsApiDscResourceBaseWithKey : AzDevOpsApiDscResourceBase - { - [System.String]$ApiDscResourceBaseId + It 'Should not throw' { - [DscProperty(Key)] - [System.String]$ApiDscResourceBaseKey + $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ + ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' + ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' } - It 'Should not throw' { + {$azDevOpsApiDscResourceBaseWithKey.GetResourceKeyPropertyName()} | Should -Not -Throw + } - $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ - ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' - ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' - } + It 'Should return the same name as the DSC Resource/class without the expected prefix' { - {$azDevOpsApiDscResourceBaseWithKey.GetResourceKeyPropertyName()} | Should -Not -Throw + $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ + ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' + ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' } - It 'Should return the same name as the DSC Resource/class without the expected prefix' { - - $azDevOpsApiDscResourceBaseWithKey = [AzDevOpsApiDscResourceBaseWithKey]@{ - ApiDscResourceBaseId = 'ApiDscResourceBaseIdValue' - ApiDscResourceBaseKey = 'ApiDscResourceBaseKeyValue' - } - - $azDevOpsApiDscResourceBaseWithKey.GetResourceKeyPropertyName() | Should -Be 'ApiDscResourceBaseKey' - } + $azDevOpsApiDscResourceBaseWithKey.GetResourceKeyPropertyName() | Should -Be 'ApiDscResourceBaseKey' } } } + diff --git a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceName.Tests.ps1 b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceName.Tests.ps1 index e8d4e87ee..d9466bf26 100644 --- a/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceName.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsApiDscResourceBase/GetResourceName.Tests.ps1 @@ -1,81 +1,65 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsApiDscResourceBase]::GetResourceName() Tests" -Tag 'Unit', 'AzDevOpsApiDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + $DscResourcePrefix = 'AzDevOps' + Context 'When called from instance of the class without the correct/expected, DSC Resource prefix' { - Describe "$script:subModuleName\Classes\AzDevOpsApiDscResourceBase\$script:commandName" -Tag $script:tag { + class DscResourceWithWrongPrefix : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$DscKey - - $DscResourcePrefix = 'AzDevOps' - - Context 'When called from instance of the class without the correct/expected, DSC Resource prefix' { - - class DscResourceWithWrongPrefix : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [DscProperty(Key)] - [string]$DscKey - - [string]GetResourceName() - { - return 'DscResourceWithWrongPrefix' - } + return 'DscResourceWithWrongPrefix' } - $dscResourceWithWrongPrefix = [DscResourceWithWrongPrefix]@{} + } + $dscResourceWithWrongPrefix = [DscResourceWithWrongPrefix]@{} - It 'Should not throw' { + It 'Should not throw' { - $dscResourceWithWrongPrefix = [DscResourceWithWrongPrefix]::new() + $dscResourceWithWrongPrefix = [DscResourceWithWrongPrefix]::new() - {$dscResourceWithWrongPrefix.GetResourceName()} | Should -Not -Throw - } + {$dscResourceWithWrongPrefix.GetResourceName()} | Should -Not -Throw + } - It 'Should return the same name as the DSC Resource/class' { + It 'Should return the same name as the DSC Resource/class' { - $dscResourceWithWrongPrefix = [DscResourceWithWrongPrefix]::new() + $dscResourceWithWrongPrefix = [DscResourceWithWrongPrefix]::new() - $dscResourceWithWrongPrefix.GetResourceName() | Should -Be 'DscResourceWithWrongPrefix' - } + $dscResourceWithWrongPrefix.GetResourceName() | Should -Be 'DscResourceWithWrongPrefix' } + } - Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { + Context 'When called from instance of the class with the correct/expected, DSC Resource prefix' { - class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) - { - [DscProperty(Key)] - [string]$DscKey + class AzDevOpsApiDscResourceBaseExample : AzDevOpsApiDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$DscKey - [string]GetResourceName() - { - return 'ApiDscResourceBaseExample' - } + [string]GetResourceName() + { + return 'ApiDscResourceBaseExample' } + } - It 'Should not throw' { + It 'Should not throw' { - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - {$azDevOpsApiDscResourceBase.GetResourceName()} | Should -Not -Throw - } + {$azDevOpsApiDscResourceBase.GetResourceName()} | Should -Not -Throw + } - It 'Should return the same name as the DSC Resource/class without the expected prefix' { + It 'Should return the same name as the DSC Resource/class without the expected prefix' { - $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() + $azDevOpsApiDscResourceBase = [AzDevOpsApiDscResourceBaseExample]::new() - $azDevOpsApiDscResourceBase.GetResourceName() | Should -Be 'AzDevOpsApiDscResourceBaseExample'.Replace('AzDevOps','') - } + $azDevOpsApiDscResourceBase.GetResourceName() | Should -Be 'AzDevOpsApiDscResourceBaseExample'.Replace('AzDevOps','') } } } + diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/AzDevOpsDscResourceBase.Initialization.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/AzDevOpsDscResourceBase.Initialization.Tests.ps1 deleted file mode 100644 index e9f0ef435..000000000 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/AzDevOpsDscResourceBase.Initialization.Tests.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 - -# Initialize tests for module function 'Classes' -. $PSScriptRoot\..\Classes.TestInitialization.ps1 - -# Note: Use of this functionality seems to pre-load the module and classes which subsquent tests can use -# which works around difficulty of referencing classes in 'source' directory when code coverage is -# using the dynamically/build-defined, 'output' directory. -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - -} diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/BeforeAll.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/BeforeAll.ps1 new file mode 100644 index 000000000..0dc71e37c --- /dev/null +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/BeforeAll.ps1 @@ -0,0 +1,8 @@ +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObject.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObject.Tests.ps1 index 9b20b1196..be235c0ee 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObject.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObject.Tests.ps1 @@ -1,109 +1,88 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 +Describe "[AzDevOpsDscResourceBase]::GetDscCurrentStateObject() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 + Context 'When no "DscCurrentStateResourceObject" object returned' { -InModuleScope 'AzureDevOpsDsc' { + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { - - - Context 'When no "DscCurrentStateResourceObject" object returned'{ - - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } - - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } - - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return $null - } + return 'AzDevOpsDscResourceBaseExample' } - It 'Should not throw' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - - {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObject()} | Should -Not -Throw + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} } - It 'Should return an object with "Ensure" property value of "Absent"' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObject().Ensure | Should -Be 'Absent' + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return $null } + } + It 'Should not throw' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + + {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObject()} | Should -Not -Throw } + It 'Should return an object with "Ensure" property value of "Absent"' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObject().Ensure | Should -Be 'Absent' + } - Context 'When no "DscCurrentStateResourceObject" object returned'{ + } - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) - { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + Context 'When no "DscCurrentStateResourceObject" object returned' { - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return [PSObject]@{ - Ensure = 'Present' - } - } + [string]GetResourceName() + { + return 'AzDevOpsDscResourceBaseExample' } - It 'Should not throw' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} + } - {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObject()} | Should -Not -Throw + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return [PSObject]@{ + Ensure = 'Present' + } } + } - It 'Should return an object with "Ensure" property value of "Present"' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should not throw' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObject().Ensure | Should -Be 'Present' - } + {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObject()} | Should -Not -Throw + } + It 'Should return an object with "Ensure" property value of "Present"' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObject().Ensure | Should -Be 'Present' } } + } diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObjectGetParameters.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObjectGetParameters.Tests.ps1 index 05aae6bc0..9f7d563fd 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObjectGetParameters.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateObjectGetParameters.Tests.ps1 @@ -1,193 +1,175 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 +Describe "[AzDevOpsDscResourceBase]::GetDscCurrentStateObjectGetParameters() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 -InModuleScope 'AzureDevOpsDsc' { + Context 'When a "ResourceId" property value is present'{ - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$ApiUri = 'https://some.api/_apis/' + [DscProperty(Key)] + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleKey = 'AzDevOpsDscResourceBaseExampleKeyValue' - - Context 'When a "ResourceId" property value is present'{ - - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - - - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } - - - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleKey = 'AzDevOpsDscResourceBaseExampleKeyValue' - - [string]GetResourceKeyPropertyName() - { - return 'AzDevOpsDscResourceBaseExampleKey' - } - - [string]GetResourceKey() - { - return 'AzDevOpsDscResourceBaseExampleKeyValue' - } - + return 'AzDevOpsDscResourceBaseExample' + } - [DscProperty()] - [string]$AzDevOpsDscResourceBaseExampleId = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID + [string]GetResourceKeyPropertyName() + { + return 'AzDevOpsDscResourceBaseExampleKey' + } - [string]GetResourceIdPropertyName() - { - return 'AzDevOpsDscResourceBaseExampleId' - } + [string]GetResourceKey() + { + return 'AzDevOpsDscResourceBaseExampleKeyValue' + } - [string]GetResourceId() - { - return '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - } + [DscProperty()] + [string]$AzDevOpsDscResourceBaseExampleId = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID + [string]GetResourceIdPropertyName() + { + return 'AzDevOpsDscResourceBaseExampleId' } - It 'Should not throw' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - - {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()} | Should -Not -Throw + [string]GetResourceId() + { + return '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID } - It 'Should return an object with "ApiUri" property value equal to object instance "ApiUri" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().ApiUri | Should -Be $azDevOpsDscResourceBaseExample.ApiUri - } + } - It 'Should return an object with "Pat" property value equal to object instance "Pat" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should not throw' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()} | Should -Not -Throw + } - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().Pat | Should -Be $azDevOpsDscResourceBaseExample.Pat - } + It 'Should return an object with "ApiUri" property value equal to object instance "ApiUri" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().ApiUri | Should -Be $azDevOpsDscResourceBaseExample.ApiUri + } - It 'Should return an object with "ResourceKey" property value equal to object instance "ResourceKey" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should return an object with "Pat" property value equal to object instance "Pat" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().Pat | Should -Be $azDevOpsDscResourceBaseExample.Pat + } - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" | - Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" - } + It 'Should return an object with "ResourceKey" property value equal to object instance "ResourceKey" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - It 'Should return an object with "ResourceId" property value equal to object instance "ResourceId" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" | + Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" + } - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | - Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" - } + It 'Should return an object with "ResourceId" property value equal to object instance "ResourceId" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - It 'Should return an object with "ResourceId" property value that is not $null' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | + Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" + } - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | - Should -Not -BeNullOrEmpty - } + It 'Should return an object with "ResourceId" property value that is not $null' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | + Should -Not -BeNullOrEmpty } + } - Context 'When a "ResourceId" property value is not present'{ - - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) - { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' + Context 'When a "ResourceId" property value is not present'{ - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$ApiUri = 'https://some.api/_apis/' + [DscProperty(Key)] + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleKey = 'AzDevOpsDscResourceBaseExampleKeyValue' + [string]GetResourceName() + { + return 'AzDevOpsDscResourceBaseExample' + } - [string]GetResourceKeyPropertyName() - { - return 'AzDevOpsDscResourceBaseExampleKey' - } - [string]GetResourceKey() - { - return 'AzDevOpsDscResourceBaseExampleKeyValue' - } + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleKey = 'AzDevOpsDscResourceBaseExampleKeyValue' + [string]GetResourceKeyPropertyName() + { + return 'AzDevOpsDscResourceBaseExampleKey' + } - [DscProperty()] - [string]$AzDevOpsDscResourceBaseExampleId + [string]GetResourceKey() + { + return 'AzDevOpsDscResourceBaseExampleKeyValue' + } - [string]GetResourceIdPropertyName() - { - return 'AzDevOpsDscResourceBaseExampleId' - } - [string]GetResourceId() - { - return $null - } + [DscProperty()] + [string]$AzDevOpsDscResourceBaseExampleId + [string]GetResourceIdPropertyName() + { + return 'AzDevOpsDscResourceBaseExampleId' + } + [string]GetResourceId() + { + return $null } - It 'Should not throw' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()} | Should -Not -Throw - } + } - It 'Should return an object with "ApiUri" property value equal to object instance "ApiUri" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should not throw' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().ApiUri | Should -Be $azDevOpsDscResourceBaseExample.ApiUri - } + {$azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()} | Should -Not -Throw + } - It 'Should return an object with "Pat" property value equal to object instance "Pat" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should return an object with "ApiUri" property value equal to object instance "ApiUri" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().Pat | Should -Be $azDevOpsDscResourceBaseExample.Pat - } + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().ApiUri | Should -Be $azDevOpsDscResourceBaseExample.ApiUri + } - It 'Should return an object with "ResourceKey" property value equal to object instance "ResourceKey" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should return an object with "Pat" property value equal to object instance "Pat" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" | - Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" - } + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters().Pat | Should -Be $azDevOpsDscResourceBaseExample.Pat + } - It 'Should return an object with "ResourceId" property value equal to object instance "ResourceId" value' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should return an object with "ResourceKey" property value equal to object instance "ResourceKey" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | - Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" - } + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" | + Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceKeyPropertyName())" + } - It 'Should return an object with "ResourceId" property value that is $null' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should return an object with "ResourceId" property value equal to object instance "ResourceId" value' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | - Should -BeNullOrEmpty - } + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | + Should -Be $azDevOpsDscResourceBaseExample."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" + } + + It 'Should return an object with "ResourceId" property value that is $null' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBaseExample.GetDscCurrentStateObjectGetParameters()."$($azDevOpsDscResourceBaseExample.GetResourceIdPropertyName())" | + Should -BeNullOrEmpty } } + } diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateResourceObject.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateResourceObject.Tests.ps1 index a4d9ffe62..edb40be2d 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateResourceObject.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetDscCurrentStateResourceObject.Tests.ps1 @@ -1,118 +1,102 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsDscResourceBase]::GetDscCurrentStateResourceObject() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + Context 'When no "DscCurrentStateResourceObject" object returned'{ + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - Context 'When no "DscCurrentStateResourceObject" object returned'{ - - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } + return 'AzDevOpsDscResourceBaseExample' + } - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} + } - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return $null - } + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return $null + } - [string]GetResourceFunctionName([RequiredAction]$RequiredAction) - { - return 'Get-Module' - } - [Hashtable]GetDesiredStateParameters([Hashtable]$Current, [Hashtable]$Desired, [RequiredAction]$RequiredAction) - { - return @{ - Name = 'SomeModuleThatWillNotExist' - } + [string]GetResourceFunctionName([RequiredAction]$RequiredAction) + { + return 'Get-Module' + } + [Hashtable]GetDesiredStateParameters([Hashtable]$Current, [Hashtable]$Desired, [RequiredAction]$RequiredAction) + { + return @{ + Name = 'SomeModuleThatWillNotExist' } } + } - It 'Should not throw' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + It 'Should not throw' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - {$azDevOpsDscResourceBaseExample.GetDscCurrentStateResourceObject(@{})} | Should -Not -Throw - } + {$azDevOpsDscResourceBaseExample.GetDscCurrentStateResourceObject(@{})} | Should -Not -Throw } + } - Context 'When no "DscCurrentStateResourceObject" object returned'{ + Context 'When no "DscCurrentStateResourceObject" object returned'{ - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) - { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } + [string]GetResourceName() + { + return 'AzDevOpsDscResourceBaseExample' + } - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} + } - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return [PSObject]@{ - Ensure = 'Present' - } + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return [PSObject]@{ + Ensure = 'Present' } + } - [string]GetResourceFunctionName([RequiredAction]$RequiredAction) - { - return 'Get-Module' - } - [Hashtable]GetDesiredStateParameters([Hashtable]$Current, [Hashtable]$Desired, [RequiredAction]$RequiredAction) - { - return @{ - Name = 'SomeModuleThatWillNotExist' - } + [string]GetResourceFunctionName([RequiredAction]$RequiredAction) + { + return 'Get-Module' + } + [Hashtable]GetDesiredStateParameters([Hashtable]$Current, [Hashtable]$Desired, [RequiredAction]$RequiredAction) + { + return @{ + Name = 'SomeModuleThatWillNotExist' } } + } - It 'Should not throw' { - $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() - - {$azDevOpsDscResourceBaseExample.GetDscCurrentStateResourceObject(@{})} | Should -Not -Throw - } + It 'Should not throw' { + $azDevOpsDscResourceBaseExample = [AzDevOpsDscResourceBaseExample]::new() + {$azDevOpsDscResourceBaseExample.GetDscCurrentStateResourceObject(@{})} | Should -Not -Throw } } + } + diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetPostSetWaitTimeSeconds.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetPostSetWaitTimeSeconds.Tests.ps1 index 31f4b584b..c01aa8ce9 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/GetPostSetWaitTimeSeconds.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/GetPostSetWaitTimeSeconds.Tests.ps1 @@ -1,65 +1,49 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsDscResourceBase]::GetPostSetWaitTimeSeconds() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { - - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } - - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + return 'AzDevOpsDscResourceBaseExample' + } - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return $null - } + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} } - Context 'When no "Set()" method is invoked'{ + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return $null + } + } - It 'Should not throw' { + Context 'When no "Set()" method is invoked'{ - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + It 'Should not throw' { - { $azDevOpsDscResourceBase.GetPostSetWaitTimeMs() } | Should -Not -Throw - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - It 'Should return $null' { + { $azDevOpsDscResourceBase.GetPostSetWaitTimeMs() } | Should -Not -Throw + } - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + It 'Should return $null' { - $azDevOpsDscResourceBase.GetPostSetWaitTimeMs() | Should -Be 2000 - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + $azDevOpsDscResourceBase.GetPostSetWaitTimeMs() | Should -Be 2000 } } + } + diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/Set.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/Set.Tests.ps1 index 12a0eda99..920235a53 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/Set.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/Set.Tests.ps1 @@ -1,69 +1,52 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsDscResourceBase]::Set() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { - - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } - - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + return 'AzDevOpsDscResourceBaseExample' + } - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return $null - } + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} } - Context 'When no "Set()" method is invoked'{ + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return $null + } + } - It 'Should not throw' { + Context 'When no "Set()" method is invoked'{ - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$setToDesiredState = {} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name SetToDesiredState -Value $setToDesiredState -Force + It 'Should not throw' { - { $azDevOpsDscResourceBase.Set() } | Should -Not -Throw - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$setToDesiredState = {} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name SetToDesiredState -Value $setToDesiredState -Force - It 'Should return $null' { + { $azDevOpsDscResourceBase.Set() } | Should -Not -Throw + } - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$setToDesiredState = {} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name SetToDesiredState -Value $setToDesiredState -Force + It 'Should return $null' { - $azDevOpsDscResourceBase.Set() | Should -BeNullOrEmpty - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$setToDesiredState = {} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name SetToDesiredState -Value $setToDesiredState -Force + $azDevOpsDscResourceBase.Set() | Should -BeNullOrEmpty } } + } diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/SetToDesiredState.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/SetToDesiredState.Tests.ps1 index 5038ec86e..210e4bf37 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/SetToDesiredState.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/SetToDesiredState.Tests.ps1 @@ -1,138 +1,123 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 -InModuleScope 'AzureDevOpsDsc' { +Describe "[AzDevOpsDscResourceBase]::SetToDesiredState() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } - - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + return 'AzDevOpsDscResourceBaseExample' + } - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return $null - } + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} + } - [string]GetResourceFunctionName([RequiredAction]$RequiredAction) - { - return 'Get-Module' - } - [Hashtable]GetDesiredStateParameters([Hashtable]$Current, [Hashtable]$Desired, [RequiredAction]$RequiredAction) - { - return @{ - Name = 'SomeModuleThatWillNotExist' - } - } + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return $null + } - [Int32]GetPostSetWaitTimeMs() - { - return 0 + [string]GetResourceFunctionName([RequiredAction]$RequiredAction) + { + return 'Get-Module' + } + [Hashtable]GetDesiredStateParameters([Hashtable]$Current, [Hashtable]$Desired, [RequiredAction]$RequiredAction) + { + return @{ + Name = 'SomeModuleThatWillNotExist' } } - $testCasesValidRequiredActionThatDoNotRequireAction = @( - @{ - RequiredAction = [RequiredAction]::Get - }, - @{ - RequiredAction = [RequiredAction]::Test - }, - @{ - RequiredAction = [RequiredAction]::Error - } - ) - - $testCasesValidRequiredActionThatRequireAction = @( - @{ - RequiredAction = [RequiredAction]::New - }, - @{ - RequiredAction = [RequiredAction]::Set - }, - @{ - RequiredAction = [RequiredAction]::Remove - } - ) + [Int32]GetPostSetWaitTimeMs() + { + return 0 + } + } + $testCasesValidRequiredActionThatDoNotRequireAction = @( + @{ + RequiredAction = [RequiredAction]::Get + }, + @{ + RequiredAction = [RequiredAction]::Test + }, + @{ + RequiredAction = [RequiredAction]::Error + } + ) + + $testCasesValidRequiredActionThatRequireAction = @( + @{ + RequiredAction = [RequiredAction]::New + }, + @{ + RequiredAction = [RequiredAction]::Set + }, + @{ + RequiredAction = [RequiredAction]::Remove + } + ) - Context 'When no "GetDscRequiredAction()" method returns a "RequiredAction" that requires an action'{ - It 'Should not throw - ""' -TestCases $testCasesValidRequiredActionThatRequireAction { - param ([RequiredAction]$RequiredAction) + Context 'When no "GetDscRequiredAction()" method returns a "RequiredAction" that requires an action'{ - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + It 'Should not throw - ""' -TestCases $testCasesValidRequiredActionThatRequireAction { + param ([RequiredAction]$RequiredAction) - { $azDevOpsDscResourceBase.SetToDesiredState() } | Should -Not -Throw - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force - It 'Should return $null - ""' -TestCases $testCasesValidRequiredActionThatDoNotRequireAction { - param ([RequiredAction]$RequiredAction) + { $azDevOpsDscResourceBase.SetToDesiredState() } | Should -Not -Throw + } - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + It 'Should return $null - ""' -TestCases $testCasesValidRequiredActionThatDoNotRequireAction { + param ([RequiredAction]$RequiredAction) - [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - $azDevOpsDscResourceBase.SetToDesiredState() | Should -BeNullOrEmpty - } + [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + $azDevOpsDscResourceBase.SetToDesiredState() | Should -BeNullOrEmpty } + } - Context 'When no "GetDscRequiredAction()" method returns a "RequiredAction" that requires no action'{ - It 'Should not throw - ""' -TestCases $testCasesValidRequiredActionThatDoNotRequireAction { - param ([RequiredAction]$RequiredAction) + Context 'When no "GetDscRequiredAction()" method returns a "RequiredAction" that requires no action'{ - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + It 'Should not throw - ""' -TestCases $testCasesValidRequiredActionThatDoNotRequireAction { + param ([RequiredAction]$RequiredAction) - { $azDevOpsDscResourceBase.SetToDesiredState() } | Should -Not -Throw - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force - It 'Should return $null - ""' -TestCases $testCasesValidRequiredActionThatDoNotRequireAction { - param ([RequiredAction]$RequiredAction) + { $azDevOpsDscResourceBase.SetToDesiredState() } | Should -Not -Throw + } - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + It 'Should return $null - ""' -TestCases $testCasesValidRequiredActionThatDoNotRequireAction { + param ([RequiredAction]$RequiredAction) - $azDevOpsDscResourceBase.SetToDesiredState() | Should -BeNullOrEmpty - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + $azDevOpsDscResourceBase.SetToDesiredState() | Should -BeNullOrEmpty } } + } + diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/Test.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/Test.Tests.ps1 index 42f4a46d6..8d88ca8f2 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/Test.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/Test.Tests.ps1 @@ -1,92 +1,76 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 +Describe "[AzDevOpsDscResourceBase]::Test() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { -InModuleScope 'AzureDevOpsDsc' { + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { - - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' - - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } - - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + return 'AzDevOpsDscResourceBaseExample' + } - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return $null - } + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} } - Context 'When no "TestDesiredState()" returns $true'{ + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return $null + } + } - It 'Should not throw' { + Context 'When no "TestDesiredState()" returns $true'{ - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$testDesiredState = {return $true} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force + It 'Should not throw' { - { $azDevOpsDscResourceBase.Test() } | Should -BeTrue - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$testDesiredState = {return $true} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force - It 'Should return $true' { + { $azDevOpsDscResourceBase.Test() } | Should -BeTrue + } - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$testDesiredState = {return $true} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force + It 'Should return $true' { - $azDevOpsDscResourceBase.Test() | Should -BeTrue - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$testDesiredState = {return $true} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force + $azDevOpsDscResourceBase.Test() | Should -BeTrue } + } - Context 'When no "TestDesiredState()" returns $false'{ - It 'Should not throw' { + Context 'When no "TestDesiredState()" returns $false'{ - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$testDesiredState = {return $false} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force + It 'Should not throw' { - { $azDevOpsDscResourceBase.Test() } | Should -Not -Throw - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$testDesiredState = {return $false} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force - It 'Should return $false' { + { $azDevOpsDscResourceBase.Test() } | Should -Not -Throw + } - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$testDesiredState = {return $false} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force + It 'Should return $false' { - $azDevOpsDscResourceBase.Test() | Should -BeFalse - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$testDesiredState = {return $false} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name TestDesiredState -Value $testDesiredState -Force + $azDevOpsDscResourceBase.Test() | Should -BeFalse } } + } + diff --git a/tests/Unit/Classes/AzDevOpsDscResourceBase/TestDesiredState.Tests.ps1 b/tests/Unit/Classes/AzDevOpsDscResourceBase/TestDesiredState.Tests.ps1 index a88015039..1602ae7f4 100644 --- a/tests/Unit/Classes/AzDevOpsDscResourceBase/TestDesiredState.Tests.ps1 +++ b/tests/Unit/Classes/AzDevOpsDscResourceBase/TestDesiredState.Tests.ps1 @@ -1,115 +1,100 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 -InModuleScope 'AzureDevOpsDsc' { +Describe "[AzDevOpsDscResourceBase]::TestDesiredState() Tests" -Tag 'Unit', 'AzDevOpsDscResourceBase' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [string]$ApiUri = 'https://some.api/_apis/' + [string]$Pat = '1234567890123456789012345678901234567890123456789012' + [DscProperty(Key)] + [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' - Describe "$script:subModuleName\Classes\AzDevOpsDscResourceBase\$script:commandName" -Tag $script:tag { + [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID - class AzDevOpsDscResourceBaseExample : AzDevOpsDscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + [string]GetResourceName() { - [string]$ApiUri = 'https://some.api/_apis/' - [string]$Pat = '1234567890123456789012345678901234567890123456789012' + return 'AzDevOpsDscResourceBaseExample' + } + + [Hashtable]GetDscCurrentStateObjectGetParameters() + { + return @{} + } + + [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) + { + return $null + } + } - [DscProperty(Key)] - [string]$AzDevOpsDscResourceBaseExampleName = 'AzDevOpsDscResourceBaseExampleNameValue' + $testCasesValidButNotNone = @( + @{ + RequiredAction = [RequiredAction]::Get + }, + @{ + RequiredAction = [RequiredAction]::New + }, + @{ + RequiredAction = [RequiredAction]::Set + }, + @{ + RequiredAction = [RequiredAction]::Remove + }, + @{ + RequiredAction = [RequiredAction]::Test + }, + @{ + RequiredAction = [RequiredAction]::Error + } + ) - [string]$AzDevOpsDscResourceBaseExampleId # = '31e71307-09b3-4d8a-b65c-5c714f64205f' # Random GUID + Context 'When no "GetDscRequiredAction()" returns "None"'{ - [string]GetResourceName() - { - return 'AzDevOpsDscResourceBaseExample' - } + It 'Should not throw' { - [Hashtable]GetDscCurrentStateObjectGetParameters() - { - return @{} - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$getDscRequiredAction = {return [RequiredAction]::None} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force - [PSObject]GetDscCurrentStateResourceObject([Hashtable]$GetParameters) - { - return $null - } + {$azDevOpsDscResourceBase.TestDesiredState()} | Should -Not -Throw } - $testCasesValidButNotNone = @( - @{ - RequiredAction = [RequiredAction]::Get - }, - @{ - RequiredAction = [RequiredAction]::New - }, - @{ - RequiredAction = [RequiredAction]::Set - }, - @{ - RequiredAction = [RequiredAction]::Remove - }, - @{ - RequiredAction = [RequiredAction]::Test - }, - @{ - RequiredAction = [RequiredAction]::Error - } - ) - - Context 'When no "GetDscRequiredAction()" returns "None"'{ - - It 'Should not throw' { - - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$getDscRequiredAction = {return [RequiredAction]::None} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force - - {$azDevOpsDscResourceBase.TestDesiredState()} | Should -Not -Throw - } - - It 'Should return $true' { - - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$getDscRequiredAction = {return [RequiredAction]::None} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force - - $azDevOpsDscResourceBase.TestDesiredState() | Should -BeTrue - } + It 'Should return $true' { + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$getDscRequiredAction = {return [RequiredAction]::None} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + + $azDevOpsDscResourceBase.TestDesiredState() | Should -BeTrue } + } - Context 'When no "GetDscRequiredAction()" does not return "None"'{ - It 'Should not throw - ""' -TestCases $testCasesValidButNotNone { - param ([RequiredAction]$RequiredAction) + Context 'When no "GetDscRequiredAction()" does not return "None"'{ - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + It 'Should not throw - ""' -TestCases $testCasesValidButNotNone { + param ([RequiredAction]$RequiredAction) - {$azDevOpsDscResourceBase.TestDesiredState()} | Should -Not -Throw - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force - It 'Should return $false - ""' -TestCases $testCasesValidButNotNone { - param ([RequiredAction]$RequiredAction) + {$azDevOpsDscResourceBase.TestDesiredState()} | Should -Not -Throw + } - $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() - [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} - $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + It 'Should return $false - ""' -TestCases $testCasesValidButNotNone { + param ([RequiredAction]$RequiredAction) - $azDevOpsDscResourceBase.TestDesiredState() | Should -BeFalse - } + $azDevOpsDscResourceBase = [AzDevOpsDscResourceBaseExample]::new() + [ScriptBlock]$getDscRequiredAction = {return $RequiredAction} + $azDevOpsDscResourceBase | Add-Member -MemberType ScriptMethod -Name GetDscRequiredAction -Value $getDscRequiredAction -Force + $azDevOpsDscResourceBase.TestDesiredState() | Should -BeFalse } } + } + diff --git a/tests/Unit/Classes/Classes.TestInitialization.ps1 b/tests/Unit/Classes/Classes.TestInitialization.ps1 deleted file mode 100644 index aeea1dc63..000000000 --- a/tests/Unit/Classes/Classes.TestInitialization.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -<# - .SYNOPSIS - Automated unit test for classes in AzureDevOpsDsc. -#> - -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestHelper.psm1') -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestCases.psm1') - -$script:dscModuleName = 'AzureDevOpsDsc' -$script:dscModule = Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1 -$script:dscModuleFile = $($script:dscModule.ModuleBase +'\'+ $script:dscModuleName + ".psd1") -Get-Module -Name $script:dscModuleName -All | - Remove-Module $script:dscModuleName -Force -ErrorAction SilentlyContinue - -$script:subModuleName = 'AzureDevOpsDsc.Common' -Import-Module -Name $script:dscModuleFile -Force - -Get-Module -Name $script:subModuleName -All | - Remove-Module -Force -ErrorAction SilentlyContinue -$script:subModulesFolder = Join-Path -Path $script:dscModule.ModuleBase -ChildPath 'Modules' -$script:subModuleFile = Join-Path $script:subModulesFolder "$($script:subModuleName)/$($script:subModuleName).psd1" -Import-Module -Name $script:subModuleFile -Force #-Verbose diff --git a/tests/Unit/Classes/DscResourceBase/BeforeAll.ps1 b/tests/Unit/Classes/DscResourceBase/BeforeAll.ps1 new file mode 100644 index 000000000..0dc71e37c --- /dev/null +++ b/tests/Unit/Classes/DscResourceBase/BeforeAll.ps1 @@ -0,0 +1,8 @@ +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} diff --git a/tests/Unit/Classes/DscResourceBase/DscResourceBase.Initialization.Tests.ps1 b/tests/Unit/Classes/DscResourceBase/DscResourceBase.Initialization.Tests.ps1 deleted file mode 100644 index b9e289e79..000000000 --- a/tests/Unit/Classes/DscResourceBase/DscResourceBase.Initialization.Tests.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -# Initialize tests for module function 'Classes' -. $PSScriptRoot\..\Classes.TestInitialization.ps1 - -# Note: Use of this functionality seems to pre-load the module and classes which subsquent tests can use -# which works around difficulty of referencing classes in 'source' directory when code coverage is -# using the dynamically/build-defined, 'output' directory. -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - -} diff --git a/tests/Unit/Classes/DscResourceBase/GetDscResourceKey.Tests.ps1 b/tests/Unit/Classes/DscResourceBase/GetDscResourceKey.Tests.ps1 index 7a6ddb6d3..73b9f2a11 100644 --- a/tests/Unit/Classes/DscResourceBase/GetDscResourceKey.Tests.ps1 +++ b/tests/Unit/Classes/DscResourceBase/GetDscResourceKey.Tests.ps1 @@ -1,83 +1,65 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 +Describe "[DscResourceBase]::GetDscResourceKey() Tests" -Tag 'Unit', 'DscResourceBase' { -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 -InModuleScope 'AzureDevOpsDsc' { + Context 'When called from instance of the class without a DSC Resource key' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + It 'Should throw' { - - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { + $dscResourceBase = [DscResourceBase]::new() + {$dscResourceBase.GetDscResourceKey()} | Should -Throw + } - Context 'When called from instance of the class without a DSC Resource key' { + Context 'When "GetDscResourceKeyPropertyName" returns a $null value' { It 'Should throw' { $dscResourceBase = [DscResourceBase]::new() + $dscResourceBase | Add-Member -MemberType ScriptMethod 'GetDscResourceKeyPropertyName' -Value { return $null } -Force {$dscResourceBase.GetDscResourceKey()} | Should -Throw } + } - Context 'When "GetDscResourceKeyPropertyName" returns a $null value' { - - It 'Should throw' { - - $dscResourceBase = [DscResourceBase]::new() - $dscResourceBase | Add-Member -MemberType ScriptMethod 'GetDscResourceKeyPropertyName' -Value { return $null } -Force - - {$dscResourceBase.GetDscResourceKey()} | Should -Throw - } - } - - - Context 'When "GetDscResourceKeyPropertyName" returns a "" (empty string) value' { + Context 'When "GetDscResourceKeyPropertyName" returns a "" (empty string) value' { - It 'Should throw' { + It 'Should throw' { - $dscResourceBase = [DscResourceBase]::new() - $dscResourceBase | Add-Member -MemberType ScriptMethod 'GetDscResourceKeyPropertyName' -Value { return '' } -Force + $dscResourceBase = [DscResourceBase]::new() + $dscResourceBase | Add-Member -MemberType ScriptMethod 'GetDscResourceKeyPropertyName' -Value { return '' } -Force - {$dscResourceBase.GetDscResourceKey()} | Should -Throw - } + {$dscResourceBase.GetDscResourceKey()} | Should -Throw } + } - } - + } - Context 'When called from instance of a class with multiple DSC Resource keys' { - It 'Should throw' { + Context 'When called from instance of a class with multiple DSC Resource keys' { - class DscResourceBase2DscKeys : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) - { - [DscProperty(Key)] - [string]$DscKey1 + It 'Should throw' { - [DscProperty(Key)] - [string]$DscKey2 - } - $dscResourceWith2Keys = [DscResourceBase2DscKeys]@{} + class DscResourceBase2DscKeys : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$DscKey1 - {$dscResourceWith2Keys.GetDscResourceKey()} | Should -Throw + [DscProperty(Key)] + [string]$DscKey2 } - + $dscResourceWith2Keys = [DscResourceBase2DscKeys]@{} + {$dscResourceWith2Keys.GetDscResourceKey()} | Should -Throw } + } + - Context 'When called from instance of class with a DSC key' { + Context 'When called from instance of class with a DSC key' { + BeforeAll { class DscResourceBase1DscKey : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) { [DscProperty(Key)] @@ -87,16 +69,17 @@ InModuleScope 'AzureDevOpsDsc' { $dscResourceWith1Key = [DscResourceBase1DscKey]@{ DscKey1='DscKey1Value' } + } - It 'Should not throw' { + It 'Should not throw' { - {$dscResourceWith1Key.GetDscResourceKey()} | Should -Not -Throw - } + $dscResourceWith1Key.GetDscResourceKey() + {$dscResourceWith1Key.GetDscResourceKey()} | Should -Not -Throw + } - It 'Should return the value of the DSC Resource key' { + It 'Should return the value of the DSC Resource key' { - $dscResourceWith1Key.GetDscResourceKey() | Should -Be 'DscKey1Value' - } + $dscResourceWith1Key.GetDscResourceKey() | Should -Be 'DscKey1Value' } } } diff --git a/tests/Unit/Classes/DscResourceBase/GetDscResourceKeyPropertyName.Tests.ps1 b/tests/Unit/Classes/DscResourceBase/GetDscResourceKeyPropertyName.Tests.ps1 index e7a6085bc..1b0d58c76 100644 --- a/tests/Unit/Classes/DscResourceBase/GetDscResourceKeyPropertyName.Tests.ps1 +++ b/tests/Unit/Classes/DscResourceBase/GetDscResourceKeyPropertyName.Tests.ps1 @@ -1,59 +1,43 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 +Describe "[DscResourceBase]::GetDscResourceKeyPropertyName() Tests" -Tag 'Unit', 'DscResourceBase' { -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 -InModuleScope 'AzureDevOpsDsc' { + Context 'When called from instance of the class without a DSC Resource key' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + It 'Should throw' { + $dscResourceWithNoDscKey = [DscResourceBase]::new() - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { - - - Context 'When called from instance of the class without a DSC Resource key' { - - It 'Should throw' { - - $dscResourceWithNoDscKey = [DscResourceBase]::new() - - {$dscResourceWithNoDscKey.GetDscResourceKeyPropertyName()} | Should -Throw - } + {$dscResourceWithNoDscKey.GetDscResourceKeyPropertyName()} | Should -Throw } + } - Context 'When called from instance of a class with multiple DSC Resource keys' { - - It 'Should throw' { + Context 'When called from instance of a class with multiple DSC Resource keys' { - class DscResourceBase2Keys : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) - { - [DscProperty(Key)] - [string]$DscKey1 + It 'Should throw' { - [DscProperty(Key)] - [string]$DscKey2 - } + class DscResourceBase2Keys : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [DscProperty(Key)] + [string]$DscKey1 - $dscResourceWith2Keys = [DscResourceBase2Keys]@{ - DscKey1 = 'DscKey1Value' - DscKey2 = 'DscKey2Value' - } + [DscProperty(Key)] + [string]$DscKey2 + } - {$dscResourceWith2Keys.GetDscResourceKeyPropertyName()} | Should -Throw + $dscResourceWith2Keys = [DscResourceBase2Keys]@{ + DscKey1 = 'DscKey1Value' + DscKey2 = 'DscKey2Value' } + + {$dscResourceWith2Keys.GetDscResourceKeyPropertyName()} | Should -Throw } + } - Context 'When called from instance of class with a DSC key' { + Context 'When called from instance of class with a DSC key' { + BeforeAll { class DscResourceBase1Key : DscResourceBase { [DscProperty(Key)] @@ -63,16 +47,16 @@ InModuleScope 'AzureDevOpsDsc' { $dscResourceWith1Key = [DscResourceBase1Key]@{ DscKey1 = 'DscKey1Value' } + } - It 'Should not throw' { - - {$dscResourceWith1Key.GetDscResourceKeyPropertyName()} | Should -Not -Throw - } + It 'Should not throw' { + $dscResourceWith1Key.GetDscResourceKeyPropertyName() + {$dscResourceWith1Key.GetDscResourceKeyPropertyName()} | Should -Not -Throw + } - It 'Should return the value of the DSC Resource key' { + It 'Should return the value of the DSC Resource key' { - $dscResourceWith1Key.GetDscResourceKeyPropertyName() | Should -Be 'DscKey1' - } + $dscResourceWith1Key.GetDscResourceKeyPropertyName() | Should -Be 'DscKey1' } } } diff --git a/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNames.Tests.ps1 b/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNames.Tests.ps1 index 47472fba2..e48954b19 100644 --- a/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNames.Tests.ps1 +++ b/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNames.Tests.ps1 @@ -1,43 +1,27 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 +Describe "[DscResourceBase]::GetDscResourcePropertyNames() Tests" -Tag 'Unit', 'DscResourceBase' { -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 -InModuleScope 'AzureDevOpsDsc' { + Context 'When called from instance of the class without any DSC properties' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + It 'Should not throw' { + $dscResourceWithNoDscProperties = [DscResourceBase]::new() - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { - - - Context 'When called from instance of the class without any DSC properties' { - - It 'Should not throw' { - - $dscResourceWithNoDscProperties = [DscResourceBase]::new() - - {$dscResourceWithNoDscProperties.GetDscResourcePropertyNames()} | Should -Not -Throw - } + {$dscResourceWithNoDscProperties.GetDscResourcePropertyNames()} | Should -Not -Throw + } - It 'Should return empty array' { + It 'Should return empty array' { - $dscResourceWithNoDscProperties = [DscResourceBase]::new() + $dscResourceWithNoDscProperties = [DscResourceBase]::new() - $dscResourceWithNoDscProperties.GetDscResourcePropertyNames().Count | Should -Be 0 - } + $dscResourceWithNoDscProperties.GetDscResourcePropertyNames().Count | Should -Be 0 } + } - Context 'When called from instance of a class with multiple DSC properties' { + Context 'When called from instance of a class with multiple DSC properties' { + BeforeAll { class DscResourceBase2Properties : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) { [DscProperty()] @@ -47,28 +31,30 @@ InModuleScope 'AzureDevOpsDsc' { [string]$AnotherDscProperty } + $dscResourceWith2DscProperties = [DscResourceBase2Properties]@{ ADscProperty = 'ADscPropertyValue' AnotherDscProperty = 'AnotherDscPropertyValue' } - It 'Should not throw' { + } + + It 'Should not throw' { - { $dscResourceWith2DscProperties.GetDscResourcePropertyNames() } | Should -Not -Throw - } + { $dscResourceWith2DscProperties.GetDscResourcePropertyNames() } | Should -Not -Throw + } - It 'Should return 2 property names' { + It 'Should return 2 property names' { - $dscResourceWith2DscProperties.GetDscResourcePropertyNames().Count | Should -Be 2 - } + $dscResourceWith2DscProperties.GetDscResourcePropertyNames().Count | Should -Be 2 + } - It 'Should return the correct property names' { + It 'Should return the correct property names' { - $propertyNames = $dscResourceWith2DscProperties.GetDscResourcePropertyNames() + $propertyNames = $dscResourceWith2DscProperties.GetDscResourcePropertyNames() - $propertyNames | Should -Contain 'ADscProperty' - $propertyNames | Should -Contain 'AnotherDscProperty' - } + $propertyNames | Should -Contain 'ADscProperty' + $propertyNames | Should -Contain 'AnotherDscProperty' } } } diff --git a/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNamesWithNoSetSupport.Tests.ps1 b/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNamesWithNoSetSupport.Tests.ps1 index 70df3c168..0ac48f4df 100644 --- a/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNamesWithNoSetSupport.Tests.ps1 +++ b/tests/Unit/Classes/DscResourceBase/GetDscResourcePropertyNamesWithNoSetSupport.Tests.ps1 @@ -1,74 +1,56 @@ -using module ..\..\..\..\output\AzureDevOpsDsc\0.2.0\AzureDevOpsDsc.psm1 +Describe "[DscResourceBase]::GetDscResourcePropertyNamesWithNoSetSupport() Tests" -Tag 'Unit', 'DscResourceBase' { -# Initialize tests for module function -. $PSScriptRoot\..\Classes.TestInitialization.ps1 -InModuleScope 'AzureDevOpsDsc' { + Context 'When called from instance of a class without any DSC properties with no "Set" support' { - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) + It 'Should not throw' { + $dscResourceWithNoSetSupportProperties = [DscResourceBase]::new() - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { - - - Context 'When called from instance of a class without any DSC properties with no "Set" support' { - - It 'Should not throw' { - - $dscResourceWithNoSetSupportProperties = [DscResourceBase]::new() - - {$dscResourceWithNoSetSupportProperties.GetDscResourcePropertyNamesWithNoSetSupport()} | Should -Not -Throw - } + {$dscResourceWithNoSetSupportProperties.GetDscResourcePropertyNamesWithNoSetSupport()} | Should -Not -Throw + } - It 'Should return empty array' { + It 'Should return empty array' { - $dscResourceWithNoSetSupportProperties = [DscResourceBase]::new() + $dscResourceWithNoSetSupportProperties = [DscResourceBase]::new() - $dscResourceWithNoSetSupportProperties.GetDscResourcePropertyNamesWithNoSetSupport().Count | Should -Be 0 - } + $dscResourceWithNoSetSupportProperties.GetDscResourcePropertyNamesWithNoSetSupport().Count | Should -Be 0 } + } - Context 'When called from instance of a class with a DSC property with no "Set" support' { + Context 'When called from instance of a class with a DSC property with no "Set" support' { - class DscResourceBaseWithNoSet : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + class DscResourceBaseWithNoSet : DscResourceBase # Note: Ignore 'TypeNotFound' warning (it is available at runtime) + { + [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() { - [System.String[]]GetDscResourcePropertyNamesWithNoSetSupport() - { - return @('NoSetPropertyName1', 'NoSetPropertyName2') - } + return @('NoSetPropertyName1', 'NoSetPropertyName2') } + } - It 'Should not throw' { + It 'Should not throw' { - $dscResourceWithANoSetSupportProperty = [DscResourceBaseWithNoSet]@{} + $dscResourceWithANoSetSupportProperty = [DscResourceBaseWithNoSet]@{} - { $dscResourceWithANoSetSupportProperty.GetDscResourcePropertyNamesWithNoSetSupport() } | Should -Not -Throw - } + { $dscResourceWithANoSetSupportProperty.GetDscResourcePropertyNamesWithNoSetSupport() } | Should -Not -Throw + } - It 'Should return the correct number of DSC resource property names that do not support "SET"' { + It 'Should return the correct number of DSC resource property names that do not support "SET"' { - $dscResourceWithANoSetSupportProperty = [DscResourceBaseWithNoSet]@{} + $dscResourceWithANoSetSupportProperty = [DscResourceBaseWithNoSet]@{} - $dscResourceWithANoSetSupportProperty.GetDscResourcePropertyNamesWithNoSetSupport().Count | Should -Be 2 - } + $dscResourceWithANoSetSupportProperty.GetDscResourcePropertyNamesWithNoSetSupport().Count | Should -Be 2 + } - It 'Should return the correct DSC resource property names that do not support "SET"' { + It 'Should return the correct DSC resource property names that do not support "SET"' { - $dscResourceWithANoSetSupportProperty = [DscResourceBaseWithNoSet]@{} + $dscResourceWithANoSetSupportProperty = [DscResourceBaseWithNoSet]@{} - $propertyNames = $dscResourceWithANoSetSupportProperty.GetDscResourcePropertyNamesWithNoSetSupport() + $propertyNames = $dscResourceWithANoSetSupportProperty.GetDscResourcePropertyNamesWithNoSetSupport() - $propertyNames | Should -Contain 'NoSetPropertyName1' - $propertyNames | Should -Contain 'NoSetPropertyName2' - } + $propertyNames | Should -Contain 'NoSetPropertyName1' + $propertyNames | Should -Contain 'NoSetPropertyName2' } } } diff --git a/tests/Unit/Classes/Resources/000.BeforeAll.ps1 b/tests/Unit/Classes/Resources/000.BeforeAll.ps1 new file mode 100644 index 000000000..3186cd374 --- /dev/null +++ b/tests/Unit/Classes/Resources/000.BeforeAll.ps1 @@ -0,0 +1,11 @@ +# Import the module containing the AzDoGroupPermission class +# Describe block for AzDoGroupPermission tests + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} diff --git a/tests/Unit/Classes/Resources/009.AzDoGroupPermission.tests.ps1 b/tests/Unit/Classes/Resources/009.AzDoGroupPermission.tests.ps1 new file mode 100644 index 000000000..789410c36 --- /dev/null +++ b/tests/Unit/Classes/Resources/009.AzDoGroupPermission.tests.ps1 @@ -0,0 +1,232 @@ + +Describe 'AzDoGroupPermission Tests' { + + BeforeAll { + $ENV:AZDODSC_CACHE_DIRECTORY = 'mocked_cache_directory' + + Mock -CommandName Import-Module + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Import-Clixml -MockWith { + return @{ + OrganizationName = 'mock-org' + Token = @{ + tokenType = 'ManagedIdentity' + access_token = 'mock_access_token' + } + + } + } + Mock -CommandName New-AzDoAuthenticationProvider + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('mock-cache-type') + } + Mock -CommandName Initialize-CacheObject + + } + AfterAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = $null + + } + + # Test case to check if the class can be instantiated + Context 'Instantiation' { + It 'Should create an instance of the AzDoGroupPermission class' { + $groupPermission = [AzDoGroupPermission]::new() + $groupPermission | Should -Not -BeNullOrEmpty + $groupPermission | Should -BeOfType 'AzDoGroupPermission' + } + } + + # Test case to check default values + Context 'Default Values' { + It 'Should have default value for isInherited as $true' { + $groupPermission = [AzDoGroupPermission]::new() + $groupPermission.isInherited | Should -Be $true + } + } + + # Test case to check property assignments + Context 'Property Assignments' { + It 'Should allow setting and getting GroupName property' { + $groupPermission = [AzDoGroupPermission]::new() + $groupPermission.GroupName = 'TestGroup' + $groupPermission.GroupName | Should -Be 'TestGroup' + } + + It 'Should allow setting and getting Permissions property' { + $groupPermission = [AzDoGroupPermission]::new() + $permissions = @( + @{ Permission = 'Read'; Allow = $true }, + @{ Permission = 'Write'; Allow = $false } + ) + $groupPermission.Permissions = $permissions + $groupPermission.Permissions | Should -Be $permissions + } + } + + # Test case for Get method + Context 'Get Method' { + + BeforeAll { + Mock -CommandName Get-AzDoGroupPermission { + + $properties = @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + GroupName = 'TestGroup' + Permissions = @{ + 'mock-permission' = @{ + Permission = 'Read' + Allow = $true + } + } + isInherited = $false + status = $null + reason = $null + } + + return $properties + + } + + } + + It 'Should return current state properties' { + + $groupPermission = [AzDoGroupPermission]::new() + $groupPermission.GroupName = 'TestGroup' + $groupPermission.isInherited = $false + $groupPermission.Permissions = @( + @{ Permission = 'Read'; Allow = $true } + ) + + $currentState = $groupPermission.Get() + + $currentState.GroupName | Should -Be 'TestGroup' + $currentState.isInherited | Should -Be $false + $currentState.Permissions | Should -Not -BeNullOrEmpty + + Assert-MockCalled Get-AzDoGroupPermission -Exactly 1 + + } + } + + Context 'Test Method' { + + BeforeAll { + + } + + It 'Should return $true when calling the test method - when the current state is unchanged' { + + Mock -CommandName Get-AzDoGroupPermission { + + $properties = @{ + Ensure = [Ensure]::Present + propertiesChanged = @() + GroupName = 'TestGroup' + Permissions = @{ + 'mock-permission' = @{ + Permission = 'Read' + Allow = $true + } + } + isInherited = $false + status = [DSCGetSummaryState]::Unchanged + reason = $null + } + + return $properties + + } + + $groupPermission = [AzDoGroupPermission]::new() + $groupPermission.GroupName = 'TestGroup' + $groupPermission.Permissions = @( + @{ Permission = 'Read'; Allow = $true } + ) + + $groupPermission.Test() | Should -Be $true + + + } + + It 'Should return $false when calling the test method - when the current state is changed' { + + Mock -CommandName Get-AzDoGroupPermission { + + $properties = @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + GroupName = 'TestGroup' + Permissions = @{ + 'mock-permission' = @{ + Permission = 'Read' + Allow = $true + } + } + isInherited = $false + status = [DSCGetSummaryState]::Missing + reason = $null + } + + return $properties + + } + + $groupPermission = [AzDoGroupPermission]::new() + $groupPermission.GroupName = 'DifferentGroup' + $groupPermission.Permissions = @( + @{ Permission = 'Read'; Allow = $true } + ) + + $groupPermission.Test() | Should -Be $false + + } + + It 'Should return $false when calling the test method - when the status is null' { + + Mock -CommandName Get-AzDoGroupPermission { + + $properties = @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + GroupName = 'TestGroup' + Permissions = @{ + 'mock-permission' = @{ + Permission = 'Read' + Allow = $true + } + } + isInherited = $false + status = $null + reason = $null + } + + return $properties + + } + + Mock -CommandName New-InvalidOperationException { + throw 'Invalid Operation Exception' + } -ParameterFilter { + $Message -like "*Could not obtain a valid 'LookupResult.Status' value within*" + } + + $groupPermission = [AzDoGroupPermission]::new() + $groupPermission.GroupName = 'TestGroup' + $groupPermission.Permissions = @( + @{ Permission = 'Read'; Allow = $true } + ) + + { $groupPermission.Test() } | Should -Not -Throw + $groupPermission.Test() | Should -Be $false + Assert-MockCalled New-InvalidOperationException -Times 1 + + } + + + } + +} diff --git a/tests/Unit/Classes/Resources/011.AzDoOrganizationGroup.tests.ps1 b/tests/Unit/Classes/Resources/011.AzDoOrganizationGroup.tests.ps1 new file mode 100644 index 000000000..a06d935bf --- /dev/null +++ b/tests/Unit/Classes/Resources/011.AzDoOrganizationGroup.tests.ps1 @@ -0,0 +1,90 @@ +# Requires -Module Pester -Version 5.0.0 +# Requires -Module DscResource.Common + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'AzDoOrganizationGroup' { + + BeforeAll { + $ENV:AZDODSC_CACHE_DIRECTORY = 'mocked_cache_directory' + + Mock -CommandName Import-Module + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Import-Clixml -MockWith { + return @{ + OrganizationName = 'mock-org' + Token = @{ + tokenType = 'ManagedIdentity' + access_token = 'mock_access_token' + } + + } + } + Mock -CommandName New-AzDoAuthenticationProvider + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('mock-cache-type') + } + Mock -CommandName Initialize-CacheObject + + } + AfterAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = $null + + } + + Context 'Constructor' { + It 'should initialize properties correctly when given valid parameters' { + $organizationGroup = [AzDoOrganizationGroup]::new() + $organizationGroup.GroupName = "MyGroup" + $organizationGroup.GroupDescription = "This is my group." + + $organizationGroup.GroupName | Should -Be "MyGroup" + $organizationGroup.GroupDescription | Should -Be "This is my group." + } + } + + Context 'GetDscResourcePropertyNamesWithNoSetSupport Method' { + It 'should return an empty array' { + $organizationGroup = [AzDoOrganizationGroup]::new() + + $result = $organizationGroup.GetDscResourcePropertyNamesWithNoSetSupport() + + $result | Should -Be @() + } + } + + Context 'GetDscCurrentStateProperties Method' { + It 'should return properties with Ensure set to Absent if CurrentResourceObject is null' { + $organizationGroup = [AzDoOrganizationGroup]::new() + + $result = $organizationGroup.GetDscCurrentStateProperties($null) + + $result.Ensure | Should -Be 'Absent' + } + + It 'should return current state properties from CurrentResourceObject' { + $organizationGroup = [AzDoOrganizationGroup]::new() + $currentResourceObject = [PSCustomObject]@{ + GroupName = "MyGroup" + GroupDescription = "This is my group" + Ensure = "Present" + LookupResult = @{ Status = "Found" } + } + + $result = $organizationGroup.GetDscCurrentStateProperties($currentResourceObject) + + $result.GroupName | Should -Be "MyGroup" + $result.GroupDescription | Should -Be "This is my group" + $result.Ensure | Should -Be "Present" + $result.LookupResult.Status | Should -Be "Found" + } + } +} diff --git a/tests/Unit/Classes/Resources/020.AzDoProject.tests.ps1 b/tests/Unit/Classes/Resources/020.AzDoProject.tests.ps1 new file mode 100644 index 000000000..64c82c130 --- /dev/null +++ b/tests/Unit/Classes/Resources/020.AzDoProject.tests.ps1 @@ -0,0 +1,120 @@ +# Requires -Module Pester -Version 5.0.0 +# Requires -Module DscResource.Common + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe "AzDoProject Class" { + + BeforeAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = 'mocked_cache_directory' + + $TestProjectNameFunctionpath = Get-FunctionItem 'Test-AzDevOpsProjectName.ps1' + . $TestProjectNameFunctionpath + + Mock -CommandName Import-Module + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Import-Clixml -MockWith { + return @{ + OrganizationName = 'mock-org' + Token = @{ + tokenType = 'ManagedIdentity' + access_token = 'mock_access_token' + } + + } + } + Mock -CommandName New-AzDoAuthenticationProvider + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('mock-cache-type') + } + Mock -CommandName Initialize-CacheObject + Mock -CommandName Test-AzDevOpsProjectName -MockWith { return $true } + + } + + Context "Initialization" { + It "Should initialize with default values" { + $project = [AzDoProject]::new() + + # Validate default values + $project.SourceControlType | Should -Be 'Git' + $project.ProcessTemplate | Should -Be 'Agile' + $project.Visibility | Should -Be 'Private' + } + } + + Context "Property Assignment" { + It "Should allow setting ProjectName and ProjectDescription" { + $project = [AzDoProject]::new() + $project.ProjectName = 'TestProject' + $project.ProjectDescription = 'This is a test project' + + # Validate assigned values + $project.ProjectName | Should -Be 'TestProject' + $project.ProjectDescription | Should -Be 'This is a test project' + } + + It "Should validate SourceControlType" { + $project = [AzDoProject]::new() + + # Valid value + { $project.SourceControlType = 'Tfvc' } | Should -Not -Throw + + # Invalid value + { $project.SourceControlType = 'InvalidValue' } | Should -Throw + } + + It "Should validate ProcessTemplate" { + $project = [AzDoProject]::new() + + # Valid value + { $project.ProcessTemplate = 'Scrum' } | Should -Not -Throw + + # Invalid value + { $project.ProcessTemplate = 'InvalidValue' } | Should -Throw + } + + It "Should validate Visibility" { + $project = [AzDoProject]::new() + + # Valid value + { $project.Visibility = 'Public' } | Should -Not -Throw + + # Invalid value + { $project.Visibility = 'InvalidValue' } | Should -Throw + } + } + + Context "Get Method" { + It "Should return an instance of AzDoProject" { + + Mock -CommandName Test-AzDevOpsProjectName -MockWith { return $true } + Mock -CommandName Get-AzDoProject -MockWith { + return @{ + Ensure = [Ensure]::Absent + ProjectName = 'MyProject' + ProjectDescription = 'This is a sample project' + SourceControlType = 'Git' + ProcessTemplate = 'Agile' + Visibility = 'Private' + propertiesChanged = @() + status = $null + } + } + + $project = [AzDoProject]::new() + $project.ProjectName = 'MyProject' + $result = $project.Get() + + $result | Should -BeOfType 'AzDoProject' + } + } +} diff --git a/tests/Unit/Classes/Resources/022.AzDoProjectGroup.tests.ps1 b/tests/Unit/Classes/Resources/022.AzDoProjectGroup.tests.ps1 new file mode 100644 index 000000000..a5641a37d --- /dev/null +++ b/tests/Unit/Classes/Resources/022.AzDoProjectGroup.tests.ps1 @@ -0,0 +1,73 @@ +# Requires -Module Pester -Version 5.0.0 +# Requires -Module DscResource.Common + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'AzDoProjectGroup' { + + BeforeAll { + $ENV:AZDODSC_CACHE_DIRECTORY = 'mocked_cache_directory' + + Mock -CommandName Import-Module + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Import-Clixml -MockWith { + return @{ + OrganizationName = 'mock-org' + Token = @{ + tokenType = 'ManagedIdentity' + access_token = 'mock_access_token' + } + + } + } + Mock -CommandName New-AzDoAuthenticationProvider + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('mock-cache-type') + } + Mock -CommandName Initialize-CacheObject + + } + AfterAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = $null + + } + + Context 'When getting the current state of a project group' { + + BeforeAll { + Mock -CommandName Get-AzDoProjectGroup -MockWith { + return @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + ProjectName = "MyProject" + GroupName = "MyGroup" + GroupDescription = "This is my project group." + } + } + } + + It 'Should return the current state properties' { + # Arrange + $projectGroup = [AzDoProjectGroup]::new() + $projectGroup.ProjectName = "MyProject" + $projectGroup.GroupName = "MyGroup" + $projectGroup.GroupDescription = "This is my project group." + + # Act + $currentState = $projectGroup.Get() + + # Assert + $currentState.GroupName | Should -Be "MyGroup" + $currentState.ProjectName | Should -Be "MyProject" + $currentState.GroupDescription | Should -Be "This is my project group." + } + } +} diff --git a/tests/Unit/Classes/Resources/031.AzDoGroupMember.tests.ps1 b/tests/Unit/Classes/Resources/031.AzDoGroupMember.tests.ps1 new file mode 100644 index 000000000..a4caf1796 --- /dev/null +++ b/tests/Unit/Classes/Resources/031.AzDoGroupMember.tests.ps1 @@ -0,0 +1,70 @@ +# Requires -Module Pester -Version 5.0.0 +# Requires -Module DscResource.Common + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'AzDoGroupMember' { + + BeforeAll { + $ENV:AZDODSC_CACHE_DIRECTORY = 'mocked_cache_directory' + + Mock -CommandName Import-Module + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Import-Clixml -MockWith { + return @{ + OrganizationName = 'mock-org' + Token = @{ + tokenType = 'ManagedIdentity' + access_token = 'mock_access_token' + } + + } + } + Mock -CommandName New-AzDoAuthenticationProvider + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('mock-cache-type') + } + Mock -CommandName Initialize-CacheObject + + } + AfterAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = $null + + } + + Context 'When getting the current state of group members' { + + BeforeAll { + Mock -CommandName Get-AzDoGroupMember -MockWith { + return @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + GroupName = "MyGroup" + GroupMembers = @("User1", "User2") + } + } + } + + It 'Should return the current state properties' { + # Arrange + $groupMember = [AzDoGroupMember]::new() + $groupMember.GroupName = "MyGroup" + $groupMember.GroupMembers = @("User1", "User2") + + # Act + $currentState = $groupMember.Get() + + # Assert + $currentState.GroupName | Should -Be "MyGroup" + $currentState.GroupMembers | Should -Be @("User1", "User2") + } + } +} diff --git a/tests/Unit/Classes/Resources/040.AzDoGitRepository.tests.ps1 b/tests/Unit/Classes/Resources/040.AzDoGitRepository.tests.ps1 new file mode 100644 index 000000000..aad7dca8a --- /dev/null +++ b/tests/Unit/Classes/Resources/040.AzDoGitRepository.tests.ps1 @@ -0,0 +1,77 @@ +# Requires -Module Pester -Version 5.0.0 +# Requires -Module DscResource.Common + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'AzDoGitRepository' { + + BeforeAll { + $ENV:AZDODSC_CACHE_DIRECTORY = 'mocked_cache_directory' + + Mock -CommandName Import-Module + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Import-Clixml -MockWith { + return @{ + OrganizationName = 'mock-org' + Token = @{ + tokenType = 'ManagedIdentity' + access_token = 'mock_access_token' + } + + } + } + Mock -CommandName New-AzDoAuthenticationProvider + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('mock-cache-type') + } + Mock -CommandName Initialize-CacheObject + + } + AfterAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = $null + + } + + + Context 'When getting the current state of a Git repository' { + + BeforeAll { + Mock -CommandName Get-AzDoGitRepository -MockWith { + return @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + ProjectName = "MyProject" + RepositoryName = "MyRepository" + SourceRepository = 'https://github.com/MyUser/MyRepository.git' + } + } + } + + It 'Should return the current state properties' { + # Arrange + $gitRepository = [AzDoGitRepository]::new() + $gitRepository.ProjectName = "MyProject" + $gitRepository.RepositoryName = "MyRepository" + + # Act + $currentState = $gitRepository.Get() + + # Assert + $currentState.ProjectName | Should -Be "MyProject" + $currentState.RepositoryName | Should -Be "MyRepository" + $currentState.SourceRepository | Should -BeNullOrEmpty + $currentState.LookupResult | Should -Not -BeNullOrEmpty + $currentState.LookupResult.ProjectName | Should -Be "MyProject" + $currentState.LookupResult.RepositoryName | Should -Be "MyRepository" + $currentState.LookupResult.SourceRepository | Should -Be 'https://github.com/MyUser/MyRepository.git' + } + } +} diff --git a/tests/Unit/Classes/Resources/041.AzDoGitPermission.tests.ps1 b/tests/Unit/Classes/Resources/041.AzDoGitPermission.tests.ps1 new file mode 100644 index 000000000..1945457b0 --- /dev/null +++ b/tests/Unit/Classes/Resources/041.AzDoGitPermission.tests.ps1 @@ -0,0 +1,76 @@ +# Requires -Module Pester -Version 5.0.0 +# Requires -Module DscResource.Common + +# Test if the class is defined +if ($null -eq $Global:ClassesLoaded) +{ + # Attempt to find the root of the repository + $RepositoryRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + # Load the Dependencies + . "$RepositoryRoot\azuredevopsdsc.tests.ps1" -LoadModulesOnly +} + +Describe 'AzDoGitPermission' { + + BeforeAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = 'mocked_cache_directory' + + Mock -CommandName Import-Module + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Import-Clixml -MockWith { + return @{ + OrganizationName = 'mock-org' + Token = @{ + tokenType = 'ManagedIdentity' + access_token = 'mock_access_token' + } + + } + } + Mock -CommandName New-AzDoAuthenticationProvider + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('mock-cache-type') + } + Mock -CommandName Initialize-CacheObject + + } + AfterAll { + + $ENV:AZDODSC_CACHE_DIRECTORY = $null + + } + + + Context 'When getting the current state of Git permissions' { + + BeforeAll { + Mock -CommandName Get-AzDoGitPermission -MockWith { + return @{ + Ensure = [Ensure]::Absent + propertiesChanged = @() + ProjectName = "MyProject" + RepositoryName = "MyRepository" + isInherited = $true + Permissions = @('Read', 'Contribute') + } + } + } + + It 'Should return the current state properties' { + # Arrange + $gitPermission = [AzDoGitPermission]::new() + $gitPermission.ProjectName = "MyProject" + $gitPermission.RepositoryName = "MyRepository" + + # Act + $currentState = $gitPermission.Get() + + # Assert + $currentState.ProjectName | Should -Be "MyProject" + $currentState.RepositoryName | Should -Be "MyRepository" + $currentState.isInherited | Should -Be $true + $currentState.LookupResult.Permissions | Should -Be @('Read', 'Contribute') + } + } +} diff --git a/tests/Unit/DSCClassResources/AzDevOpsProject/AzDevOpsProject.Tests.ps1 b/tests/Unit/DSCClassResources/AzDevOpsProject/AzDevOpsProject.Tests.ps1 deleted file mode 100644 index 0decd03ac..000000000 --- a/tests/Unit/DSCClassResources/AzDevOpsProject/AzDevOpsProject.Tests.ps1 +++ /dev/null @@ -1,301 +0,0 @@ -# Initialize tests for module function -. $PSScriptRoot\..\DSCClassResources.TestInitialization.ps1 - -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) - - - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { - - $validDscMethodNames = @( - 'Get', - 'Set', - 'Test' - ) - $testCasesValidDscMethodNames = $validDscMethodNames | ForEach-Object { - @{ - MethodName = $_ - } - } - - $validPropertyNames = @( - 'ApiUri', - 'Pat', - 'ProjectId', - 'ProjectName', - 'ProjectDescription' - ) - $testCasesValidPropertyNames = $validPropertyNames | ForEach-Object { - @{ - PropertyName = $_ - PropertyValue = $_ + "Value" - } - } - - $testCasesValidPropertyNames += @{ - PropertyName = 'SourceControlType' - PropertyValue = 'Git' - } - - Context 'When creating a new instance of the class' { - - It 'Should not throw' { - - {[AzDevOpsProject]::new()} | Should -Not -Throw - } - } - - - Context 'When evaluating properties of the class' { - - It 'Should contain expected property - ""' -TestCases $testCasesValidPropertyNames { - param ([System.String]$PropertyName) - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.PSobject.Properties.Name | Should -Contain $PropertyName - } - - It 'Should contain expected property value - ""' -TestCases $testCasesValidPropertyNames { - param ([System.String]$PropertyName, [System.String]$PropertyValue) - - $azDevOpsProject = [AzDevOpsProject]@{ - "$PropertyName" = $PropertyValue - } - - $azDevOpsProject."$PropertyName" | Should -Be $PropertyValue - } - } - - - Context 'When evaluating DSC methods of the class' { - - It 'Should contain expected method - ""' -TestCases $testCasesValidDscMethodNames { - param ([System.String]$MethodName) - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.PSobject.Methods.Name | Should -Contain $MethodName - } - - - } - } -} - - - - - - - - -# <# -# .SYNOPSIS -# Automated unit test for AzDevOpsProject DSC Resource. -# #> - -# $script:dscModuleName = 'AzureDevOpsDsc' -# $script:dscResourceName = 'AzDevOpsProject' - -# function Invoke-TestSetup -# { -# try -# { -# Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' -# } -# catch [System.IO.FileNotFoundException] -# { -# throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' -# } - -# $script:testEnvironment = Initialize-TestEnvironment ` -# -DSCModuleName $script:dscModuleName ` -# -DSCResourceName $script:dscResourceName ` -# -ResourceType 'Class' ` -# -TestType 'Unit' -# } - -# function Invoke-TestCleanup -# { -# Restore-TestEnvironment -TestEnvironment $script:testEnvironment -# } - -# # Begin Testing - -# Invoke-TestSetup - -# try -# { -# InModuleScope $script:dscResourceName { -# Set-StrictMode -Version 1.0 - -# Describe 'AzDevOpsProject\Parameters' -Tag 'Parameter' { -# BeforeAll { -# #$mockInstanceName = 'DSCTEST' - -# #Mock -CommandName Import-SQLPSModule -# } -# } - -# Describe 'AzDevOpsProject\Get' -Tag 'Get' { - - - -# BeforeAll { - -# $getApiUri = "https://www.someUri.api/_apis/" -# $getPat = "1234567890123456789012345678901234567890123456789012" - -# $getProjectId = [GUID]::NewGuid().ToString() -# $getProjectName = "ProjectName_$projectId" -# $getProjectDescription = "ProjectDescription_$projectId" - -# $getAzDevOpsResource = @{ -# id = $getProjectId -# name = $getProjectName -# description = $getProjectDescription -# } - -# $AzDevOpsProjectResource = [AzDevOpsProject]@{ -# ApiUri = $getApiUri -# Pat = $getPat -# ProjectId = $getProjectId -# ProjectName = $getProjectName -# ProjectDescription = $getProjectDescription -# } -# } - - -# Context 'When Azure DevOps is not in the desired state' { -# Context 'When the Azure DevOps "Project" does not exist' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $null -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult | Should -Be $null -# $getResult.ApiUri | Should -Be $null -# $getResult.Pat | Should -Be $null -# $getResult.ProjectId | Should -Be $null -# $getResult.ProjectName | Should -Be $null -# $getResult.ProjectDescription | Should -Be $null -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectId" parameter is different' { -# BeforeAll { -# $differentProjectId = [GUID]::NewGuid().ToString() -# $getAzDevOpsResource.ProjectId = $differentProjectId - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectId" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Not -Be $differentProjectId # Different -# $getResult.ProjectName | Should -Be $getProjectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectName" parameter is different' { -# BeforeAll { -# $differentProjectName = "z" + $getAzDevOpsResource.ProjectName -# $getAzDevOpsResource.ProjectName = $differentProjectName - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectName" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Not -Be $differentProjectName # Different -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - -# Context 'When the Azure DevOps "Project" exists but "ProjectDescription" parameter is different' { -# BeforeAll { -# $differentProjectDescription = "z" + $getAzDevOpsResource.ProjectDescription -# $getAzDevOpsResource.ProjectDescription = $differentProjectDescription - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectDescription" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Not -Be $differentProjectDescription # Different -# } -# } -# } - -# Context 'When Azure DevOps is in the desired state' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } - -# } -# } - -# } -# } -# finally -# { -# Invoke-TestCleanup -# } diff --git a/tests/Unit/DSCClassResources/AzDevOpsProject/Get.Tests.ps1 b/tests/Unit/DSCClassResources/AzDevOpsProject/Get.Tests.ps1 deleted file mode 100644 index 4a96d9ab0..000000000 --- a/tests/Unit/DSCClassResources/AzDevOpsProject/Get.Tests.ps1 +++ /dev/null @@ -1,262 +0,0 @@ -# Initialize tests for module function -. $PSScriptRoot\..\DSCClassResources.TestInitialization.ps1 - -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) - - - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { - - Context 'When calling Get() method' { - - $azDevOpsProjectProperties = @{ - Ensure = 'Present' - ApiUri = 'https://some.api.uri/_apis/' - Pat = '1234567890123456789012345678901234567890123456789012' - ProjectId = 'efb9c508-115d-4380-a038-51f970d7f918' # Random GUID - ProjectName = 'SomeProjectName' - ProjectDescription = 'SomeProjectDescription' - SourceControlType = 'Git' - } - - $azDevOpsProjectApiResource = [PSObject]@{ - id = 'efb9c508-115d-4380-a038-51f970d7f918' # Random GUID - name = 'SomeProjectName' - description = 'SomeProjectDescription' - capabilities = @{ - versioncontrol = @{ - sourceControlType = 'Git' - } - } - } - - $azDevOpsProject = [AzDevOpsProject]$azDevOpsProjectProperties - - # Override/mock the GetDscCurrentStateProperties() method in this class - [ScriptBlock]$GetDscCurrentStateProperties = {return [PSObject]$azDevOpsProjectProperties} - $azDevOpsProject | Add-Member -MemberType ScriptMethod -Name 'GetDscCurrentStateProperties' -Value $GetDscCurrentStateProperties -Force - - It 'Should not throw' { - - {$azDevOpsProject.Get()} | Should -Not -Throw - } - } - } -} - - - - - - - - -# <# -# .SYNOPSIS -# Automated unit test for AzDevOpsProject DSC Resource. -# #> - -# $script:dscModuleName = 'AzureDevOpsDsc' -# $script:dscResourceName = 'AzDevOpsProject' - -# function Invoke-TestSetup -# { -# try -# { -# Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' -# } -# catch [System.IO.FileNotFoundException] -# { -# throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' -# } - -# $script:testEnvironment = Initialize-TestEnvironment ` -# -DSCModuleName $script:dscModuleName ` -# -DSCResourceName $script:dscResourceName ` -# -ResourceType 'Class' ` -# -TestType 'Unit' -# } - -# function Invoke-TestCleanup -# { -# Restore-TestEnvironment -TestEnvironment $script:testEnvironment -# } - -# # Begin Testing - -# Invoke-TestSetup - -# try -# { -# InModuleScope $script:dscResourceName { -# Set-StrictMode -Version 1.0 - -# Describe 'AzDevOpsProject\Parameters' -Tag 'Parameter' { -# BeforeAll { -# #$mockInstanceName = 'DSCTEST' - -# #Mock -CommandName Import-SQLPSModule -# } -# } - -# Describe 'AzDevOpsProject\Get' -Tag 'Get' { - - - -# BeforeAll { - -# $getApiUri = "https://www.someUri.api/_apis/" -# $getPat = "1234567890123456789012345678901234567890123456789012" - -# $getProjectId = [GUID]::NewGuid().ToString() -# $getProjectName = "ProjectName_$projectId" -# $getProjectDescription = "ProjectDescription_$projectId" - -# $getAzDevOpsResource = @{ -# id = $getProjectId -# name = $getProjectName -# description = $getProjectDescription -# } - -# $AzDevOpsProjectResource = [AzDevOpsProject]@{ -# ApiUri = $getApiUri -# Pat = $getPat -# ProjectId = $getProjectId -# ProjectName = $getProjectName -# ProjectDescription = $getProjectDescription -# } -# } - - -# Context 'When Azure DevOps is not in the desired state' { -# Context 'When the Azure DevOps "Project" does not exist' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $null -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult | Should -Be $null -# $getResult.ApiUri | Should -Be $null -# $getResult.Pat | Should -Be $null -# $getResult.ProjectId | Should -Be $null -# $getResult.ProjectName | Should -Be $null -# $getResult.ProjectDescription | Should -Be $null -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectId" parameter is different' { -# BeforeAll { -# $differentProjectId = [GUID]::NewGuid().ToString() -# $getAzDevOpsResource.ProjectId = $differentProjectId - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectId" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Not -Be $differentProjectId # Different -# $getResult.ProjectName | Should -Be $getProjectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectName" parameter is different' { -# BeforeAll { -# $differentProjectName = "z" + $getAzDevOpsResource.ProjectName -# $getAzDevOpsResource.ProjectName = $differentProjectName - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectName" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Not -Be $differentProjectName # Different -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - -# Context 'When the Azure DevOps "Project" exists but "ProjectDescription" parameter is different' { -# BeforeAll { -# $differentProjectDescription = "z" + $getAzDevOpsResource.ProjectDescription -# $getAzDevOpsResource.ProjectDescription = $differentProjectDescription - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectDescription" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Not -Be $differentProjectDescription # Different -# } -# } -# } - -# Context 'When Azure DevOps is in the desired state' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } - -# } -# } - -# } -# } -# finally -# { -# Invoke-TestCleanup -# } diff --git a/tests/Unit/DSCClassResources/AzDevOpsProject/GetDscCurrentStateProperties.Tests.ps1 b/tests/Unit/DSCClassResources/AzDevOpsProject/GetDscCurrentStateProperties.Tests.ps1 deleted file mode 100644 index 103642a10..000000000 --- a/tests/Unit/DSCClassResources/AzDevOpsProject/GetDscCurrentStateProperties.Tests.ps1 +++ /dev/null @@ -1,366 +0,0 @@ -# Initialize tests for module function -. $PSScriptRoot\..\DSCClassResources.TestInitialization.ps1 - -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) - - - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { - - - $azDevOpsProjectProperties = @{ - Ensure = 'Present' - ApiUri = 'https://some.api.uri/_apis/' - Pat = '1234567890123456789012345678901234567890123456789012' - ProjectId = 'efb9c508-115d-4380-a038-51f970d7f918' # Random GUID - ProjectName = 'SomeProjectName' - ProjectDescription = 'SomeProjectDescription' - SourceControlType = 'Git' - } - - $azDevOpsProjectApiResource = [PSObject]@{ - id = 'efb9c508-115d-4380-a038-51f970d7f918' # Random GUID - name = 'SomeProjectName' - description = 'SomeProjectDescription' - capabilities = @{ - versioncontrol = @{ - sourceControlType = 'Git' - } - } - } - - $currentResourceObjectThatExists = [PSObject]$azDevOpsProjectApiResource - $currentResourceObjectThatDoesNotExist = [PSObject]@{} - - - $testCasesValidAzDevOpsProjectProperties = $azDevOpsProjectProperties.Keys | ForEach-Object { - @{ - PropertyName = $_ - } - } - $testCasesValidAzDevOpsProjectPropertiesWhenNotExists = $testCasesValidAzDevOpsProjectProperties | - Where-Object { $_.PropertyName -in @('Ensure', 'ApiUri', 'Pat') } - - - Context 'When current resource already exists' { - - It 'Should not throw' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - {$azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatExists)} | Should -Not -Throw - } - - It 'Should return a hashtable with expected key/property - ""' -TestCases $testCasesValidAzDevOpsProjectProperties { - param ([System.String]$PropertyName) - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatExists).ContainsKey($PropertyName) | Should -Be $true - } - - It 'Should return a hashtable with an "Ensure" key value of "Present"' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatExists).Ensure | Should -Be "Present" - } - - It 'Should return a hashtable with output values matching corresponding, API resource values' { - - $azDevOpsProject = [AzDevOpsProject]@{ - ProjectId = $azDevOpsProjectProperties.ProjectId - } - - $dscCurrentStateProperties = $azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatExists) - - $dscCurrentStateProperties.ProjectId | Should -Be $azDevOpsProjectApiResource.id - $dscCurrentStateProperties.ProjectName | Should -Be $azDevOpsProjectApiResource.name - $dscCurrentStateProperties.ProjectDescription | Should -Be $azDevOpsProjectApiResource.description - $dscCurrentStateProperties.SourceControlType | Should -Be $azDevOpsProjectApiResource.capabilities.versioncontrol.sourceControlType - } - - It 'Should return a hashtable with output values matching corresponding, non-API resource values' { - - $azDevOpsProject = [AzDevOpsProject]@{ - ApiUri = $azDevOpsProjectProperties.ApiUri - Pat = $azDevOpsProjectProperties.Pat - } - - $dscCurrentStateProperties = $azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatExists) - - $dscCurrentStateProperties.ApiUri | Should -Be $azDevOpsProjectProperties.ApiUri - $dscCurrentStateProperties.Pat | Should -Be $azDevOpsProjectProperties.Pat - } - } - - - Context 'When current resource does not exist (not $null but with no "id" property)' { - - It 'Should not throw' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - {$azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatDoesNotExist)} | Should -Not -Throw - } - - It 'Should return a hashtable with expected key/property - ""' -TestCases $testCasesValidAzDevOpsProjectPropertiesWhenNotExists { - param ([System.String]$PropertyName) - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatDoesNotExist).ContainsKey($PropertyName) | Should -Be $true - } - - It 'Should return a hashtable with an "Ensure" key value of "Absent"' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscCurrentStateProperties($currentResourceObjectThatDoesNotExist).Ensure | Should -Be "Absent" - } - } - - - Context 'When current resource is null' { - - It 'Should not throw' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - {$azDevOpsProject.GetDscCurrentStateProperties($null)} | Should -Not -Throw - } - - It 'Should return a hashtable with expected key/property - ""' -TestCases $testCasesValidAzDevOpsProjectPropertiesWhenNotExists { - param ([System.String]$PropertyName) - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscCurrentStateProperties($null).ContainsKey($PropertyName) | Should -Be $true - } - - It 'Should return a hashtable with an "Ensure" key value of "Absent"' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscCurrentStateProperties($null).Ensure | Should -Be "Absent" - } - } - } -} - - - - - - - - -# <# -# .SYNOPSIS -# Automated unit test for AzDevOpsProject DSC Resource. -# #> - -# $script:dscModuleName = 'AzureDevOpsDsc' -# $script:dscResourceName = 'AzDevOpsProject' - -# function Invoke-TestSetup -# { -# try -# { -# Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' -# } -# catch [System.IO.FileNotFoundException] -# { -# throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' -# } - -# $script:testEnvironment = Initialize-TestEnvironment ` -# -DSCModuleName $script:dscModuleName ` -# -DSCResourceName $script:dscResourceName ` -# -ResourceType 'Class' ` -# -TestType 'Unit' -# } - -# function Invoke-TestCleanup -# { -# Restore-TestEnvironment -TestEnvironment $script:testEnvironment -# } - -# # Begin Testing - -# Invoke-TestSetup - -# try -# { -# InModuleScope $script:dscResourceName { -# Set-StrictMode -Version 1.0 - -# Describe 'AzDevOpsProject\Parameters' -Tag 'Parameter' { -# BeforeAll { -# #$mockInstanceName = 'DSCTEST' - -# #Mock -CommandName Import-SQLPSModule -# } -# } - -# Describe 'AzDevOpsProject\Get' -Tag 'Get' { - - - -# BeforeAll { - -# $getApiUri = "https://www.someUri.api/_apis/" -# $getPat = "1234567890123456789012345678901234567890123456789012" - -# $getProjectId = [GUID]::NewGuid().ToString() -# $getProjectName = "ProjectName_$projectId" -# $getProjectDescription = "ProjectDescription_$projectId" - -# $getAzDevOpsResource = @{ -# id = $getProjectId -# name = $getProjectName -# description = $getProjectDescription -# } - -# $AzDevOpsProjectResource = [AzDevOpsProject]@{ -# ApiUri = $getApiUri -# Pat = $getPat -# ProjectId = $getProjectId -# ProjectName = $getProjectName -# ProjectDescription = $getProjectDescription -# } -# } - - -# Context 'When Azure DevOps is not in the desired state' { -# Context 'When the Azure DevOps "Project" does not exist' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $null -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult | Should -Be $null -# $getResult.ApiUri | Should -Be $null -# $getResult.Pat | Should -Be $null -# $getResult.ProjectId | Should -Be $null -# $getResult.ProjectName | Should -Be $null -# $getResult.ProjectDescription | Should -Be $null -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectId" parameter is different' { -# BeforeAll { -# $differentProjectId = [GUID]::NewGuid().ToString() -# $getAzDevOpsResource.ProjectId = $differentProjectId - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectId" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Not -Be $differentProjectId # Different -# $getResult.ProjectName | Should -Be $getProjectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectName" parameter is different' { -# BeforeAll { -# $differentProjectName = "z" + $getAzDevOpsResource.ProjectName -# $getAzDevOpsResource.ProjectName = $differentProjectName - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectName" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Not -Be $differentProjectName # Different -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - -# Context 'When the Azure DevOps "Project" exists but "ProjectDescription" parameter is different' { -# BeforeAll { -# $differentProjectDescription = "z" + $getAzDevOpsResource.ProjectDescription -# $getAzDevOpsResource.ProjectDescription = $differentProjectDescription - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectDescription" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Not -Be $differentProjectDescription # Different -# } -# } -# } - -# Context 'When Azure DevOps is in the desired state' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } - -# } -# } - -# } -# } -# finally -# { -# Invoke-TestCleanup -# } diff --git a/tests/Unit/DSCClassResources/AzDevOpsProject/GetDscResourcePropertyNamesWithNoSetSupport.Tests.ps1 b/tests/Unit/DSCClassResources/AzDevOpsProject/GetDscResourcePropertyNamesWithNoSetSupport.Tests.ps1 deleted file mode 100644 index b33c98d45..000000000 --- a/tests/Unit/DSCClassResources/AzDevOpsProject/GetDscResourcePropertyNamesWithNoSetSupport.Tests.ps1 +++ /dev/null @@ -1,259 +0,0 @@ -# Initialize tests for module function -. $PSScriptRoot\..\DSCClassResources.TestInitialization.ps1 - -InModuleScope 'AzureDevOpsDsc' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:dscResourceName = Split-Path $PSScriptRoot -Leaf - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Classes\$script:dscResourceName\$script:dscResourceName.psm1" - $script:tag = @($($script:commandName -replace '-')) - - - Describe "$script:subModuleName\Classes\DscResourceBase\Method\$script:commandName" -Tag $script:tag { - - $testCasesPropertyNamesWithNoSetSupport = @( - @{ - PropertyName = 'SourceControlType' - } - ) - - - Context 'When calling GetDscResourcePropertyNamesWithNoSetSupport() method' { - - It 'Should not throw' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - {$azDevOpsProject.GetDscResourcePropertyNamesWithNoSetSupport()} | Should -Not -Throw - } - - It 'Should output expected number of property names' { - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscResourcePropertyNamesWithNoSetSupport().Count | Should -Be $testCasesPropertyNamesWithNoSetSupport.Count - } - - It 'Should output expected "PropertyName" - ""' -TestCases $testCasesPropertyNamesWithNoSetSupport { - param ([System.String]$PropertyName) - - $azDevOpsProject = [AzDevOpsProject]::new() - - $azDevOpsProject.GetDscResourcePropertyNamesWithNoSetSupport() | Should -Contain $PropertyName - } - } - } -} - - - - - - - - -# <# -# .SYNOPSIS -# Automated unit test for AzDevOpsProject DSC Resource. -# #> - -# $script:dscModuleName = 'AzureDevOpsDsc' -# $script:dscResourceName = 'AzDevOpsProject' - -# function Invoke-TestSetup -# { -# try -# { -# Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' -# } -# catch [System.IO.FileNotFoundException] -# { -# throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' -# } - -# $script:testEnvironment = Initialize-TestEnvironment ` -# -DSCModuleName $script:dscModuleName ` -# -DSCResourceName $script:dscResourceName ` -# -ResourceType 'Class' ` -# -TestType 'Unit' -# } - -# function Invoke-TestCleanup -# { -# Restore-TestEnvironment -TestEnvironment $script:testEnvironment -# } - -# # Begin Testing - -# Invoke-TestSetup - -# try -# { -# InModuleScope $script:dscResourceName { -# Set-StrictMode -Version 1.0 - -# Describe 'AzDevOpsProject\Parameters' -Tag 'Parameter' { -# BeforeAll { -# #$mockInstanceName = 'DSCTEST' - -# #Mock -CommandName Import-SQLPSModule -# } -# } - -# Describe 'AzDevOpsProject\Get' -Tag 'Get' { - - - -# BeforeAll { - -# $getApiUri = "https://www.someUri.api/_apis/" -# $getPat = "1234567890123456789012345678901234567890123456789012" - -# $getProjectId = [GUID]::NewGuid().ToString() -# $getProjectName = "ProjectName_$projectId" -# $getProjectDescription = "ProjectDescription_$projectId" - -# $getAzDevOpsResource = @{ -# id = $getProjectId -# name = $getProjectName -# description = $getProjectDescription -# } - -# $AzDevOpsProjectResource = [AzDevOpsProject]@{ -# ApiUri = $getApiUri -# Pat = $getPat -# ProjectId = $getProjectId -# ProjectName = $getProjectName -# ProjectDescription = $getProjectDescription -# } -# } - - -# Context 'When Azure DevOps is not in the desired state' { -# Context 'When the Azure DevOps "Project" does not exist' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $null -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult | Should -Be $null -# $getResult.ApiUri | Should -Be $null -# $getResult.Pat | Should -Be $null -# $getResult.ProjectId | Should -Be $null -# $getResult.ProjectName | Should -Be $null -# $getResult.ProjectDescription | Should -Be $null -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectId" parameter is different' { -# BeforeAll { -# $differentProjectId = [GUID]::NewGuid().ToString() -# $getAzDevOpsResource.ProjectId = $differentProjectId - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectId" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Not -Be $differentProjectId # Different -# $getResult.ProjectName | Should -Be $getProjectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - - -# Context 'When the Azure DevOps "Project" exists but "ProjectName" parameter is different' { -# BeforeAll { -# $differentProjectName = "z" + $getAzDevOpsResource.ProjectName -# $getAzDevOpsResource.ProjectName = $differentProjectName - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectName" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Not -Be $differentProjectName # Different -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } -# } - -# Context 'When the Azure DevOps "Project" exists but "ProjectDescription" parameter is different' { -# BeforeAll { -# $differentProjectDescription = "z" + $getAzDevOpsResource.ProjectDescription -# $getAzDevOpsResource.ProjectDescription = $differentProjectDescription - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values, with "ProjectDescription" values different' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Not -Be $differentProjectDescription # Different -# } -# } -# } - -# Context 'When Azure DevOps is in the desired state' { -# BeforeAll { - -# $AzDevOpsProjectResource = $AzDevOpsProjectResource | -# Add-Member -MemberType 'ScriptMethod' -Name 'GetAzDevOpsResource' -Value { -# return $getAzDevOpsResource -# } -Force -PassThru - -# } - -# It 'Should return the correct values' { -# $getResult = $AzDevOpsProjectResource.Get() - -# $getResult.ApiUri | Should -Be $getApiUri -# $getResult.Pat | Should -Be $getPat -# $getResult.ProjectId | Should -Be $getProjectId -# $getResult.ProjectName | Should -Be $getprojectName -# $getResult.ProjectDescription | Should -Be $getProjectDescription -# } - -# } -# } - -# } -# } -# finally -# { -# Invoke-TestCleanup -# } diff --git a/tests/Unit/DSCClassResources/DSCClassResources.TestInitialization.ps1 b/tests/Unit/DSCClassResources/DSCClassResources.TestInitialization.ps1 deleted file mode 100644 index aeea1dc63..000000000 --- a/tests/Unit/DSCClassResources/DSCClassResources.TestInitialization.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -<# - .SYNOPSIS - Automated unit test for classes in AzureDevOpsDsc. -#> - -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestHelper.psm1') -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestCases.psm1') - -$script:dscModuleName = 'AzureDevOpsDsc' -$script:dscModule = Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1 -$script:dscModuleFile = $($script:dscModule.ModuleBase +'\'+ $script:dscModuleName + ".psd1") -Get-Module -Name $script:dscModuleName -All | - Remove-Module $script:dscModuleName -Force -ErrorAction SilentlyContinue - -$script:subModuleName = 'AzureDevOpsDsc.Common' -Import-Module -Name $script:dscModuleFile -Force - -Get-Module -Name $script:subModuleName -All | - Remove-Module -Force -ErrorAction SilentlyContinue -$script:subModulesFolder = Join-Path -Path $script:dscModule.ModuleBase -ChildPath 'Modules' -$script:subModuleFile = Join-Path $script:subModulesFolder "$($script:subModuleName)/$($script:subModuleName).psd1" -Import-Module -Name $script:subModuleFile -Force #-Verbose diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common.Functions.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common.Functions.Tests.ps1 deleted file mode 100644 index 51cae7660..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common.Functions.Tests.ps1 +++ /dev/null @@ -1,211 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - -return - -InModuleScope $script:subModuleName { - - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:publicCommandNames = $($(Get-Command -Module $script:subModuleName).Name) - - [hashtable[]]$testCasesValidCommandParameterSetNames = $script:publicCommandNames | ForEach-Object { - - $CommandName = $_ - $ParameterSetName = '__AllParameterSets' - $ParameterSetTestCases = $(Get-ParameterSetTestCase -CommandName $_ -ParameterSetName '__AllParameterSets' -TestCaseName 'Valid') - - $ParameterSetTestCases | ForEach-Object { - [hashtable]$testCase = $_ - $testCase.Add('CommandName',$CommandName) - $testCase.Add('ParameterSetName',$ParameterSetName) - $testCase.Add('ParameterNames',$_.ParameterSetValues.Keys) - - $testCase - } - } - - [hashtable[]]$testCasesValidCommandParameterSetNameValidParameterValues = $testCasesValidCommandParameterSetNames | Where-Object { $_.ParameterNames.Count -gt 0 } | ForEach-Object { - - $commandName = $_.CommandName - $parameterNames = $_.ParameterNames - - # Note: Exclude any parameter sets that do not have any parameters and ensure only - # looping through the test cases for the 'CommandName' being looped through in outer loop - $testCasesValidCommandParameterSetNames | Where-Object { $_.ParameterNames.Count -gt 0 -and $_.CommandName -eq $commandName } | ForEach-Object { - - $testCase = $_ - - $parameterNames | ForEach-Object { - - $parameterName = $_ - $parameterValues = $(Get-TestCaseValue -ScopeName $parameterName -TestCaseName 'Valid' -First 1) - - $parameterValues | ForEach-Object { - - $parameterValue = $_ - - if ($testCase.ParameterSetValues.ContainsKey($parameterName)) # Only want to generate new records if 'ParameterName' is in the set of 'ParameterSetValues' keys - { - $newTestCase = @{} - $testCase.Keys | ForEach-Object { - $newTestCase[$_] = $testCase[$_] - } - $newTestCase.Remove('ParameterSetValues') - $newTestCase.Add('ParameterSetValues',@{}) - $testCase.ParameterSetValues.Keys | ForEach-Object { - $newTestCase.ParameterSetValues[$_] = $testCase.ParameterSetValues[$_] - } - $newTestCase.ParameterSetValues[$parameterName] = $parameterValue - $newTestCase.Add('ParameterValue',$parameterValue) - $newTestCase.Add('ParameterName',$parameterName) - - $newTestCase - } - } - } - } - } - - - - [hashtable[]]$testCasesInvalidCommandParameterSetNames = $script:publicCommandNames | ForEach-Object { - - $CommandName = $_ - $ParameterSetName = '__AllParameterSets' - $ParameterSetTestCases = $(Get-ParameterSetTestCase -CommandName $_ -ParameterSetName '__AllParameterSets' -TestCaseName 'Invalid') - - $ParameterSetTestCases | ForEach-Object { - [hashtable]$testCase = $_ - $testCase.Add('CommandName',$CommandName) - $testCase.Add('ParameterSetName',$ParameterSetName) - $testCase.Add('ParameterNames',$_.Keys) - - $testCase - } - } - - [hashtable[]]$testCasesValidCommandParameterSetNameInvalidParameterValues = $testCasesValidCommandParameterSetNames | Where-Object { $_.ParameterNames.Count -gt 0 } | ForEach-Object { - - $commandName = $_.CommandName - $parameterNames = $_.ParameterNames - - # Note: Exclude any parameter sets that do not have any parameters and ensure only - # looping through the test cases for the 'CommandName' being looped through in outer loop - $testCasesValidCommandParameterSetNames | Where-Object { $_.ParameterNames.Count -gt 0 -and $_.CommandName -eq $commandName } | ForEach-Object { - - $testCase = $_ - - $parameterNames | ForEach-Object { - - $parameterName = $_ - $parameterValues = $(Get-TestCaseValue -ScopeName $parameterName -TestCaseName 'Invalid' -First 1) - - $parameterValues | ForEach-Object { - - if ($testCase.ParameterSetValues.ContainsKey($parameterName)) # Only want to generate new records if 'ParameterName' is in the set of 'ParameterSetValues' keys - { - $parameterValue = $_ - - $newTestCase = @{} - $testCase.Keys | ForEach-Object { - $newTestCase[$_] = $testCase[$_] - } - $newTestCase.Remove('ParameterSetValues') - $newTestCase.Add('ParameterSetValues',@{}) - $testCase.ParameterSetValues.Keys | ForEach-Object { - $newTestCase.ParameterSetValues[$_] = $testCase.ParameterSetValues[$_] - } - $newTestCase.ParameterSetValues[$parameterName] = $parameterValue - $newTestCase.Add('ParameterValue',$parameterValue) - $newTestCase.Add('ParameterName',$parameterName) - - $newTestCase - } - } - } - } - } - - - Describe "$subModuleName\AzureDevOpsDsc.Common\*\Functions" { - - - Context "When validating function/command parameter sets" { - - BeforeEach { - - Mock Invoke-AzDevOpsApiRestMethod { - return @{ - id = '14c15b78-b85d-401f-8095-504c57bbd79e' - } - } - - Mock Start-Sleep {} - #Mock New-InvalidOperationException {} # Don't mock this. Want exception to be thrown by it. - } - - Context "When invoking function/command with 'Valid', parameter set values" { - - It "Should not throw - '' - '' - " -TestCases $testCasesValidCommandParameterSetNames { - param([string]$CommandName, [Hashtable]$ParameterSetValues) - - Mock -CommandName $CommandName -MockWith {} - { & $CommandName @ParameterSetValues } | Should -Not -Throw - } - - It "Should not throw - '' - '' - ('' = '')" -TestCases $testCasesValidCommandParameterSetNameValidParameterValues { - param([string]$CommandName, [Hashtable]$ParameterSetValues) - - Mock -CommandName $CommandName -MockWith {} - { & $CommandName @ParameterSetValues } | Should -Not -Throw - } - } - - - Context "When invoking function/command with 'Invalid', parameter set values" { - - Context "When 'IsValid' parameter name is not present" { - - It "Should throw - '' - '' - " -TestCases $($testCasesInvalidCommandParameterSetNames | Where-Object { $_.ParameterSetValuesKey -notlike '*IsValid*' }) { - param([string]$CommandName, [Hashtable]$ParameterSetValues) - - Mock -CommandName $CommandName -MockWith {} - { & $CommandName @ParameterSetValues } | Should -Throw - } - - It "Should throw - '' - '' - " -TestCases $($testCasesInvalidCommandParameterSetNames | Where-Object { $_.ParameterSetValuesKey -notlike '*IsValid*' }){ - param([string]$CommandName, [Hashtable]$ParameterSetValues) - - Mock -CommandName $CommandName -MockWith {} - { & $CommandName @ParameterSetValues } | Should -Throw - } - } - - Context "When 'IsValid' parameter name is present" { - - # Don't want this to throw an exception - Typically they need to return a $false return value if input parameters are invalid. - - It "Should not throw - '' - '' - ('' = '')" -TestCases $($testCasesValidCommandParameterSetNameInvalidParameterValues | Where-Object { $_.ParameterSetValuesKey -like '*IsValid*' }) { - param([string]$CommandName, [Hashtable]$ParameterSetValues) - - Mock -CommandName $CommandName -MockWith {} - { & $CommandName @ParameterSetValues } | Should -Not -Throw - } - - It "Should not throw - '' - '' - ('' = '')" -TestCases $($testCasesValidCommandParameterSetNameInvalidParameterValues | Where-Object { $_.ParameterSetValuesKey -like '*IsValid*' }) { - param([string]$CommandName, [Hashtable]$ParameterSetValues) - - Mock -CommandName $CommandName -MockWith {} - { & $CommandName @ParameterSetValues } | Should -Not -Throw - } - - } - - - } - } - - } - -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common.Tests.Initialization.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common.Tests.Initialization.ps1 deleted file mode 100644 index aeea1dc63..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common.Tests.Initialization.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -<# - .SYNOPSIS - Automated unit test for classes in AzureDevOpsDsc. -#> - -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestHelper.psm1') -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestCases.psm1') - -$script:dscModuleName = 'AzureDevOpsDsc' -$script:dscModule = Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1 -$script:dscModuleFile = $($script:dscModule.ModuleBase +'\'+ $script:dscModuleName + ".psd1") -Get-Module -Name $script:dscModuleName -All | - Remove-Module $script:dscModuleName -Force -ErrorAction SilentlyContinue - -$script:subModuleName = 'AzureDevOpsDsc.Common' -Import-Module -Name $script:dscModuleFile -Force - -Get-Module -Name $script:subModuleName -All | - Remove-Module -Force -ErrorAction SilentlyContinue -$script:subModulesFolder = Join-Path -Path $script:dscModule.ModuleBase -ChildPath 'Modules' -$script:subModuleFile = Join-Path $script:subModulesFolder "$($script:subModuleName)/$($script:subModuleName).psd1" -Import-Module -Name $script:subModuleFile -Force #-Verbose diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsACL.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsACL.tests.ps1 new file mode 100644 index 000000000..1cc2ba464 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsACL.tests.ps1 @@ -0,0 +1,62 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Get-DevOpsACL" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-DevOpsACL.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Mock the required functions + Mock -CommandName Get-AzDevOpsApiVersion { return "6.0" } + Mock -CommandName Invoke-AzDevOpsApiRestMethod + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + } + + It "should call Invoke-AzDevOpsApiRestMethod with correct parameters" { + $orgName = "TestOrg" + $secDescId = "abc123" + $apiVersion = "6.0" + + Get-DevOpsACL -OrganizationName $orgName -SecurityDescriptorId $secDescId + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Times 1 -Exactly -ParameterFilter { + $Uri -eq "https://dev.azure.com/$orgName/_apis/accesscontrollists/$secDescId?api-version=$apiVersion" + $Method -eq "GET" + } + } + + It "should handle null response from REST API and return null" { + Mock Invoke-AzDevOpsApiRestMethod { return @{ value = $null } } + + $result = Get-DevOpsACL -OrganizationName "TestOrg" -SecurityDescriptorId "abc123" + + $result | Should -BeNull + Assert-MockCalled Add-CacheItem -Times 0 -Exactly -Scope It + Assert-MockCalled Export-CacheObject -Times 0 -Exactly -Scope It + + } + + It "should cache and return ACL list if response is not empty" { + $mockACLList = @{ count = 1; value = @("aclEntry1", "aclEntry2") } + + Mock Invoke-AzDevOpsApiRestMethod { return $mockACLList } + + $result = Get-DevOpsACL -OrganizationName "TestOrg" -SecurityDescriptorId "abc123" + + $result | Should -HaveCount 2 + Assert-MockCalled Add-CacheItem -Times 1 -Exactly -Scope It + Assert-MockCalled Export-CacheObject -Times 1 -Exactly -Scope It + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsDescriptorIdentity.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsDescriptorIdentity.tests.ps1 new file mode 100644 index 000000000..4b33f5270 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ACL/Get-DevOpsDescriptorIdentity.tests.ps1 @@ -0,0 +1,82 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-DevOpsDescriptorIdentity' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-DevOpsDescriptorIdentity.tests.ps1' + } + + # Load the functions to test + + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + $OrganizationName = "MyOrg" + $SubjectDescriptor = "subject:abcd1234" + $Descriptor = "descriptor:abcd1234" + $ApiVersion = "5.0" + + Mock -CommandName Get-AzDevOpsApiVersion { return "5.0" } + Mock -CommandName Invoke-AzDevOpsApiRestMethod + + } + + Context 'With Default Parameter Set and SubjectDescriptor' { + It 'Calls Invoke-AzDevOpsApiRestMethod with correct parameters' { + Get-DevOpsDescriptorIdentity -OrganizationName $OrganizationName -SubjectDescriptor $SubjectDescriptor + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 -Scope It -ParameterFilter { + $Uri -match "subjectDescriptors=$SubjectDescriptor" -and + $Uri -match "_apis/identities" -and + $Method -eq 'Get' + } + } + } + + Context 'With Descriptors Parameter Set' { + It 'Calls Invoke-AzDevOpsApiRestMethod with correct parameters' { + Get-DevOpsDescriptorIdentity -OrganizationName $OrganizationName -Descriptor $Descriptor + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 -Scope It -ParameterFilter { + $Uri -match "descriptors=$Descriptor" -and + $Uri -match "_apis/identities" -and + $Method -eq 'Get' + } + } + } + + Context 'Handles empty response' { + It 'Returns $null when identity value is $null' { + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return @{ value = $null; count = 0 } } + + $result = Get-DevOpsDescriptorIdentity -OrganizationName $OrganizationName -SubjectDescriptor $SubjectDescriptor + + $result | Should -BeNullOrEmpty + } + + It 'Returns $null when count is greater than 1' { + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return @{ value = @('identity1', 'identity2'); count = 2 } } + + $result = Get-DevOpsDescriptorIdentity -OrganizationName $OrganizationName -SubjectDescriptor $SubjectDescriptor + + $result | Should -BeNullOrEmpty + } + } + + Context 'Handles valid response' { + It 'Returns identity value when count is 1' { + $expectedValue = @('identity1') + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return @{ value = $expectedValue; count = 1 } } + + $result = Get-DevOpsDescriptorIdentity -OrganizationName $OrganizationName -SubjectDescriptor $SubjectDescriptor + + $result | Should -Be $expectedValue + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Remove-AzDoPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Remove-AzDoPermission.tests.ps1 new file mode 100644 index 000000000..a14cc71c0 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Remove-AzDoPermission.tests.ps1 @@ -0,0 +1,70 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-AzDoPermission' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { '5.1' } + + } + + Context 'When invoked' { + It 'Should call Invoke-AzDevOpsApiRestMethod with correct parameters' { + + # Arrange + $OrganizationName = 'ExampleOrg' + $SecurityNamespaceID = '00000000-0000-0000-0000-000000000000' + $TokenName = 'ExampleToken' + $ApiVersion = '5.1' + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return $true + } + Mock -CommandName Write-Error + + # Act + $result = Remove-AzDoPermission -OrganizationName $OrganizationName -SecurityNamespaceID $SecurityNamespaceID -TokenName $TokenName -ApiVersion $ApiVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly 1 -ParameterFilter { + $Uri -eq "https://dev.azure.com/$OrganizationName/_apis/securitynamespaces/$SecurityNamespaceID/descriptors/$TokenName?api-version=$ApiVersion" + $Method -eq 'DELETE' + } + Assert-MockCalled -CommandName Write-Error -Exactly 0 + + } + + It 'Should handle exceptions and write error message' { + + # Arrange + $OrganizationName = 'ExampleOrg' + $SecurityNamespaceID = '00000000-0000-0000-0000-000000000000' + $TokenName = 'ExampleToken' + $ApiVersion = '5.1' + + Mock -CommandName Invoke-AzDevOpsApiRestMethod + Mock -CommandName Write-Error + + # Act + $result = Remove-AzDoPermission -OrganizationName $OrganizationName -SecurityNamespaceID $SecurityNamespaceID -TokenName $TokenName -ApiVersion $ApiVersion + $result | Should -BeNullOrEmpty + + # Assert + Assert-MockCalled -CommandName Write-Error -Exactly 1 + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly 1 + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Set-AzDoPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Set-AzDoPermission.tests.ps1 new file mode 100644 index 000000000..925dcf7cd --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/AzDoPermission/Set-AzDoPermission.tests.ps1 @@ -0,0 +1,75 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-AzDoPermission Tests' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion { return '6.0-preview.1' } + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return $null } + + $OrganizationName = "TestOrg" + $SecurityNamespaceID = "TestNamespace" + $SerializedACLs = @{some = "data"} + $ApiVersion = "5.0" + + } + + + Context 'When Mandatory Parameters are provided' { + It 'Should call Invoke-AzDevOpsApiRestMethod with correct parameters' { + $expectedUri = 'https://dev.azure.com/{0}/_apis/accesscontrollists/{1}?api-version={2}' -f $OrganizationName, $SecurityNamespaceID, $ApiVersion + + Set-AzDoPermission -OrganizationName $OrganizationName -SecurityNamespaceID $SecurityNamespaceID -SerializedACLs $SerializedACLs -ApiVersion $ApiVersion + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly 1 -ParameterFilter { + $Uri -eq $expectedUri + $Method -eq 'POST' + $Body -eq $SerializedACLs + } + } + } + + Context 'When ApiVersion is not provided' { + It 'Should call ExampleFunction and get default ApiVersion' { + $expectedUri = 'https://dev.azure.com/{0}/_apis/accesscontrollists/{1}?api-version={2}' -f $OrganizationName, $SecurityNamespaceID, "6.0-preview.1" + + Set-AzDoPermission -OrganizationName $OrganizationName -SecurityNamespaceID $SecurityNamespaceID -SerializedACLs $SerializedACLs + + Assert-MockCalled -CommandName Get-AzDevOpsApiVersion -Exactly 1 + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly 1 -ParameterFilter { + $Uri -eq $expectedUri + $Method -eq 'POST' + $Body -eq $SerializedACLs + } + } + } + + Context 'When an exception occurs' { + + It 'Should catch and log the error' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { throw "API call failed" } + Mock -CommandName Write-Error + + $result = Set-AzDoPermission -OrganizationName $OrganizationName -SecurityNamespaceID $SecurityNamespaceID -SerializedACLs $SerializedACLs -ApiVersion $ApiVersion + + $result | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly 1 + Assert-MockCalled -CommandName Write-Error -Exactly -Times 1 + + } + } +} + + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGitRepository.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGitRepository.tests.ps1 new file mode 100644 index 000000000..68a7fae94 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGitRepository.tests.ps1 @@ -0,0 +1,76 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'List-DevOpsGitRepository' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-DevOpsGitRepository.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '6.0' } + Mock -CommandName Invoke-AzDevOpsApiRestMethod + } + + It 'Returns repositories when API provides data' { + $mockResult = @{ + value = @( + @{name = 'Repo1'; id = '1'}, + @{name = 'Repo2'; id = '2'} + ) + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return $mockResult } + + $result = List-DevOpsGitRepository -OrganizationName 'org' -ProjectName 'proj' + + $result | Should -HaveCount 2 + $result[0].name | Should -Be 'Repo1' + $result[1].name | Should -Be 'Repo2' + } + + It 'Returns null when API does not provide data' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { + return @{ value = $null } + } + + $result = List-DevOpsGitRepository -OrganizationName 'org' -ProjectName 'proj' + $result | Should -Be $null + + } + + It 'Uses default API version if not provided' { + $result = List-DevOpsGitRepository -OrganizationName 'org' -ProjectName 'proj' + + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 1 + } + + It 'Uses provided API version' { + $customApiVersion = '5.1' + + $result = List-DevOpsGitRepository -OrganizationName 'org' -ProjectName 'proj' -ApiVersion $customApiVersion + + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 0 + } + + It 'Calls the REST API with correct parameters' { + $dummyOrg = 'dummyOrg' + $dummyProj = 'dummyProj' + + List-DevOpsGitRepository -OrganizationName $dummyOrg -ProjectName $dummyProj + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $Uri -eq "https://dev.azure.com/$dummyOrg/$dummyProj/_apis/git/repositories" -and + $Method -eq 'Get' + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroupMembers.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroupMembers.tests.ps1 new file mode 100644 index 000000000..f53c3c5cb --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroupMembers.tests.ps1 @@ -0,0 +1,78 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'List-DevOpsGroupMembers' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-DevOpsGroupMembers.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '6.0-preview.1' } + + } + + Context 'When called with mandatory parameters' { + + # Inject expected parameters + It 'Should return group members' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + value = @( + @{principalName = 'user1@domain.com'} + @{principalName = 'user2@domain.com'} + ) + } + } + + # Inject expected parameters + $result = List-DevOpsGroupMembers -Organization 'MyOrg' -GroupDescriptor 'MyGroup' + $result | Should -Not -BeNullOrEmpty + + # Validate the result + $result[0].principalName | Should -Be 'user1@domain.com' + $result[1].principalName | Should -Be 'user2@domain.com' + + } + } + + Context 'When no members are found' { + + It 'Should return null' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { + return @{ value = $null } + } + + $result = List-DevOpsGroupMembers -Organization 'MyOrg' -GroupDescriptor 'MyGroup' + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 + $result | Should -BeNullOrEmpty + } + + } + + Context 'When optional ApiVersion parameter is provided' { + + It 'Should ignore Get-AzDevOpsApiVersion and use provided ApiVersion' { + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '5.0' } + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return $null } + + # Inject expected ApiVersion + $null = List-DevOpsGroupMembers -Organization 'MyOrg' -GroupDescriptor 'MyGroup' -ApiVersion '6.0-preview.1' + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 0 -ParameterFilter { + $Uri -eq 'https://dev.azure.com/MyOrg/_apis/graph/groups/MyGroup/members?api-version=6.0-preview.1' + } + } + + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroups.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroups.tests.ps1 new file mode 100644 index 000000000..5edd05531 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsGroups.tests.ps1 @@ -0,0 +1,78 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'List-DevOpsGroups' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-DevOpsGroups.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '6.0-preview' } + + } + + Context "When calling List-DevOpsGroups" { + + BeforeAll { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + value = @( + @{ + displayName = 'Group1' + }, + @{ + displayName = 'Group2' + } + ) + } + } + } + + It 'should call Invoke-AzDevOpsApiRestMethod' { + List-DevOpsGroups -Organization 'myOrg' + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 + } + + It 'should call Get-AzDevOpsApiVersion if no ApiVersion is specified' { + List-DevOpsGroups -Organization 'myOrg' + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 1 + } + + It 'should not call Get-AzDevOpsApiVersion if ApiVersion is specified' { + List-DevOpsGroups -Organization 'myOrg' -ApiVersion '5.1' + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 0 -ParameterFilter { + $Uri -eq 'https://dev.azure.com/myOrg/_apis/graph/groups?api-version=5.1' + } + } + + It 'should return group data' { + $result = List-DevOpsGroups -Organization 'myOrg' + $result.Count | Should -Be 2 + $result[0].displayName | Should -Be 'Group1' + $result[1].displayName | Should -Be 'Group2' + } + + } + + Context "When no groups are found" { + + BeforeAll { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { return @{ value = $null } } + } + + It 'should return null if no groups are found' { + $result = List-DevOpsGroups -Organization 'myOrg' + $result | Should -BeNullOrEmpty + } + + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProcess.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProcess.tests.ps1 new file mode 100644 index 000000000..8cc8079bf --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProcess.tests.ps1 @@ -0,0 +1,90 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'List-DevOpsProcess' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-DevOpsProcess.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return "6.0" } + + } + + # Test cases + Context 'When called with mandatory parameters' { + + # Validate the call + It 'should call Invoke-AzDevOpsApiRestMethod' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ value = @() } + } + + List-DevOpsProcess -Organization "MyOrganization" + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $apiUri -eq "https://dev.azure.com/MyOrganization/_apis/process/processes?api-version=6.0" -and + $Method -eq 'Get' + } -Times 1 + } + + it "should change the url when the api-version changes" { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ value = @() } + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return "6.1" } + + List-DevOpsProcess -Organization "MyOrganization" + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $apiUri -eq "https://dev.azure.com/MyOrganization/_apis/process/processes?api-version=6.1" -and + $Method -eq 'Get' + } -Times 1 + } + + # Validate the call + It 'should return the process groups' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + value = @( + @{ id = "1"; name = "Agile" } + @{ id = "2"; name = "Scrum" } + ) + } + } + + # Validate the result + $result = List-DevOpsProcess -Organization "MyOrganization" + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + + # Validate the first process + $result.id[0] | Should -Be "1" + $result.name[0] | Should -Be "Agile" + $result.id[1] | Should -Be "2" + $result.name[1] | Should -Be "Scrum" + + } + } + + Context 'When no processes are returned' { + + It 'should return $null' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { return @{ value = $null } } + + $result = List-DevOpsProcess -Organization "MyOrganization" + $result | Should -Be $null + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProjects.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProjects.tests.ps1 new file mode 100644 index 000000000..dd8d447f4 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsProjects.tests.ps1 @@ -0,0 +1,49 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "List-DevOpsProjects" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-DevOpsProjects.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { + return "6.0" + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + value = @( + @{ name = "Project1"; id = "123" }, + @{ name = "Project2"; id = "456" } + ) + } + } + } + + It "Returns project list when called with valid organization name" { + $result = List-DevOpsProjects -OrganizationName "TestOrg" + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + $result[0].name | Should -Be "Project1" + $result[1].name | Should -Be "Project2" + } + + It "Returns null when no projects found" { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ value = @() } + } + + $result = List-DevOpsProjects -OrganizationName "TestOrg" + $result | Should -BeNullOrEmpty + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsSecurityNamespaces.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsSecurityNamespaces.tests.ps1 new file mode 100644 index 000000000..7ac7488cc --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsSecurityNamespaces.tests.ps1 @@ -0,0 +1,67 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "List-DevOpsSecurityNamespaces" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-DevOpsSecurityNamespaces.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { + return @{ + value = @( + @{ + namespaceId = "testNamespace1" + description = "Test Namespace 1" + }, + @{ + namespaceId = "testNamespace2" + description = "Test Namespace 2" + } + ) + } + } + + } + + It "Should call Invoke-AzDevOpsApiRestMethod" { + $organizationName = "TestOrganization" + + List-DevOpsSecurityNamespaces -OrganizationName $organizationName + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $apiUri -eq "https://dev.azure.com/$organizationName/_apis/securitynamespaces/" -and + $Method -eq 'Get' + } -Times 1 + } + + It "Should return the namespaces value when present" { + $organizationName = "TestOrganization" + + $result = List-DevOpsSecurityNamespaces -OrganizationName $organizationName + $result | Should -HaveCount 2 + $result[0].namespaceId | Should -Be "testNamespace1" + $result[1].namespaceId | Should -Be "testNamespace2" + } + + It 'Should return $null when there are no namespaces' { + Mock Invoke-AzDevOpsApiRestMethod { + return @{ + value = $null + } + } + + $organizationName = "TestOrganization" + + $result = List-DevOpsSecurityNamespaces -OrganizationName $organizationName + $result | Should -BeNullOrEmpty + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsServicePrinciples.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsServicePrinciples.tests.ps1 new file mode 100644 index 000000000..8154ab171 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-DevOpsServicePrinciples.tests.ps1 @@ -0,0 +1,85 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'List-DevOpsServicePrinciples' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-DevOpsServicePrinciples.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion { + return '6.0-preview.1' + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { + return @{ + value = @( + @{ + id = 'sp1' + displayName = 'Service Principal 1' + }, + @{ + id = 'sp2' + displayName = 'Service Principal 2' + } + ) + } + } + + } + + Context 'When called with valid OrganizationName' { + It 'Returns a list of service principals' { + $result = List-DevOpsServicePrinciples -OrganizationName 'MyOrg' + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 2 + $result[0].id | Should -Be 'sp1' + $result[0].displayName | Should -Be 'Service Principal 1' + $result[1].id | Should -Be 'sp2' + $result[1].displayName | Should -Be 'Service Principal 2' + } + + It 'Uses the default API version if not provided' { + List-DevOpsServicePrinciples -OrganizationName 'MyOrg' + + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 1 -Scope It + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $Uri -eq 'https://vssps.dev.azure.com/MyOrg/_apis/graph/serviceprincipals' -and + $Method -eq 'Get' + } -Exactly 1 -Scope It + } + + It 'Uses the provided API version if specified' { + List-DevOpsServicePrinciples -OrganizationName 'MyOrg' -ApiVersion '5.0-preview.1' + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $Uri -eq 'https://vssps.dev.azure.com/MyOrg/_apis/graph/serviceprincipals' -and + $Method -eq 'Get' + } -Exactly 1 -Scope It + } + } + + Context 'When API returns null' { + + It 'Returns null' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { + return @{ value = $null } + } + + $result = List-DevOpsServicePrinciples -OrganizationName 'MyOrg' + $result | Should -BeNull + + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-UserCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-UserCache.tests.ps1 new file mode 100644 index 000000000..b78593296 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Cache/List-UserCache.tests.ps1 @@ -0,0 +1,75 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'List-UserCache' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'List-UserCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion { return "5.0-preview.1" } + Mock -CommandName Invoke-AzDevOpsApiRestMethod + + } + + Context 'when called with valid OrganizationName' { + It 'should call Invoke-AzDevOpsApiRestMethod with correct parameters' { + $OrganizationName = 'TestOrg' + $ApiVersion = '5.0-preview.1' + $response = @{ + value = @( + @{ displayName = 'User1' } + @{ displayName = 'User2' } + ) + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return $response } + + $result = List-UserCache -OrganizationName $OrganizationName -ApiVersion $ApiVersion + + $expectedUri = "https://vssps.dev.azure.com/$OrganizationName/_apis/graph/users" + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Times 1 -Exactly -Scope It -ParameterFilter { + $Uri -eq $expectedUri -and + $Method -eq 'Get' + } + + $result.Count | Should -Be 2 + $result[0].displayName | Should -Be 'User1' + $result[1].displayName | Should -Be 'User2' + } + } + + Context 'when API returns null' { + It 'should return null' { + $OrganizationName = 'TestOrg' + $ApiVersion = '5.0-preview.1' + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { return @{ value = $null } } + + $result = List-UserCache -OrganizationName $OrganizationName -ApiVersion $ApiVersion + + $result | Should -BeNullOrEmpty + } + } + + Context 'when ApiVersion is not provided' { + It 'should call Get-AzDevOpsApiVersion' { + $OrganizationName = 'TestOrg' + + Mock -CommandName Get-AzDevOpsApiVersion { return "5.0-preview.1" } + + $result = List-UserCache -OrganizationName $OrganizationName + + Assert-MockCalled -CommandName Get-AzDevOpsApiVersion -Exactly 1 + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/New-GitRepository.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/New-GitRepository.tests.ps1 new file mode 100644 index 000000000..1e7a28456 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/New-GitRepository.tests.ps1 @@ -0,0 +1,57 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'New-GitRepository Tests' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-GitRepository.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod + + } + + Context 'When creating a repository successfully' { + + It 'should invoke the REST method with correct parameters' { + $mockApiUri = "https://dev.azure.com/org" + $mockProject = [PSCustomObject]@{ name = 'TestProject'; id = '12345' } + $mockRepoName = "TestRepo" + $mockApiVersion = "5.0" + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + [PSCustomObject]@{ name = $mockRepoName } + } -Verifiable -ParameterFilter { $Body -match "TestRepo" } + + $result = New-GitRepository -ApiUri $mockApiUri -Project $mockProject -RepositoryName $mockRepoName -ApiVersion $mockApiVersion + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 + $result.name | Should -Be $mockRepoName + } + } + + Context 'When failing to create a repository' { + + It 'should throw an error and write an error message' { + $mockApiUri = "https://dev.azure.com/org" + $mockProject = [PSCustomObject]@{ name = 'TestProject'; id = '12345' } + $mockRepoName = "TestRepo" + $mockApiVersion = "5.0" + + Mock -CommandName Write-Error -Verifiable + + { New-GitRepository -ApiUri $mockApiUri -Project $mockProject -RepositoryName $mockRepoName -ApiVersion $mockApiVersion } | Should -Not -Throw + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/Remove-GitRepository.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/Remove-GitRepository.tests.ps1 new file mode 100644 index 000000000..c029c96dc --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GitRepository/Remove-GitRepository.tests.ps1 @@ -0,0 +1,63 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Remove-GitRepository" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-GitRepository.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return "5.0" } + Mock -CommandName Invoke-AzDevOpsApiRestMethod + + } + + It "Should call Invoke-AzDevOpsApiRestMethod with correct parameters" { + $ApiUri = "https://dev.azure.com/organization" + $Project = [PSCustomObject]@{ name = "SampleProject" } + $Repository = [PSCustomObject]@{ id = "123"; Name = "SampleRepo" } + + Remove-GitRepository -ApiUri $ApiUri -Project $Project -Repository $Repository + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly -Times 1 -ParameterFilter { + $ApiUri -eq "https://dev.azure.com/organization/SampleProject/_apis/git/repositories/123?api-version=5.0" -and + $Method -eq 'Delete' + } + } + + It "Should use the provided ApiVersion if specified" { + $ApiUri = "https://dev.azure.com/organization" + $Project = [PSCustomObject]@{ name = "SampleProject" } + $Repository = [PSCustomObject]@{ id = "123"; Name = "SampleRepo" } + $ApiVersion = "6.0" + + Remove-GitRepository -ApiUri $ApiUri -Project $Project -Repository $Repository -ApiVersion $ApiVersion + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly -Times 1 -ParameterFilter { + $ApiUri -eq "https://dev.azure.com/organization/SampleProject/_apis/git/repositories/123?api-version=6.0" -and + $Method -eq 'Delete' + } + } + + It "Should handle and display the error if Invoke-AzDevOpsApiRestMethod throws an exception" { + $ApiUri = "https://dev.azure.com/organization" + $Project = [PSCustomObject]@{ name = "SampleProject" } + $Repository = [PSCustomObject]@{ id = "123"; Name = "SampleRepo" } + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { throw "API Error" } + + { Remove-GitRepository -ApiUri $ApiUri -Project $Project -Repository $Repository } | Should -Not -Throw + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly -Times 1 + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/New-DevOpsGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/New-DevOpsGroup.tests.ps1 new file mode 100644 index 000000000..d31d70865 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/New-DevOpsGroup.tests.ps1 @@ -0,0 +1,78 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "New-DevOpsGroup" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-DevOpsGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + displayName = $GroupName + description = $GroupDescription + id = "mock-id" + } + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return "6.0" } + } + + Context "When required parameters are provided" { + It 'Creates a new group successfully' { + $ApiUri = "https://dev.azure.com/myorganization" + $GroupName = "MyGroup" + + $result = New-DevOpsGroup -ApiUri $ApiUri -GroupName $GroupName + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly -Times 1 + $result.displayName | Should -Be $GroupName + } + } + + Context "When optional parameters are provided" { + It 'Creates a new group successfully with description' { + $ApiUri = "https://dev.azure.com/myorganization" + $GroupName = "MyGroup" + $GroupDescription = "A sample group" + + $result = New-DevOpsGroup -ApiUri $ApiUri -GroupName $GroupName -GroupDescription $GroupDescription + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly -Times 1 + $result.description | Should -Be $GroupDescription + } + + It 'Creates a new group successfully with project scope descriptor' { + $ApiUri = "https://dev.azure.com/myorganization" + $GroupName = "MyGroup" + $ProjectScopeDescriptor = "vstfs:///Classification/TeamProject/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + + $result = New-DevOpsGroup -ApiUri $ApiUri -GroupName $GroupName -ProjectScopeDescriptor $ProjectScopeDescriptor + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly -Times 1 + $result.displayName | Should -Be $GroupName + } + } + + Context "When an exception is thrown" { + BeforeAll { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { throw "API call failed" } + Mock -CommandName Write-Error -Verifiable + } + + It 'Handles the error and writes an error message' { + $ApiUri = "https://dev.azure.com/myorganization" + $GroupName = "MyGroup" + + { New-DevOpsGroup -ApiUri $ApiUri -GroupName $GroupName } | Should -Not -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Remove-DevOpsGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Remove-DevOpsGroup.tests.ps1 new file mode 100644 index 000000000..898799b3c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Remove-DevOpsGroup.tests.ps1 @@ -0,0 +1,70 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-DevOpsGroup' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-DevOpsGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { "6.0" } + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + [PSCustomObject]@{ success = $true } + } + + $Uri = "https://dev.azure.com/myorganization" + $GroupDescriptor = "MyGroup" + $ApiVersion = "6.0" + } + + Context 'When all mandatory parameters are provided' { + It 'should make a DELETE request to Azure DevOps API' { + + $params = @{ + Uri = '{0}/_apis/graph/groups/{1}?api-version={2}' -f $Uri, $GroupDescriptor, $ApiVersion + Method = 'Delete' + } + + $result = Remove-DevOpsGroup -ApiUri $Uri -GroupDescriptor $GroupDescriptor + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly 1 -ParameterFilter { + $ApiUri -eq $params.Uri -and + $Method -eq $params.Method + } + $result.success | Should -Be $true + } + } + + Context 'When ApiVersion parameter is not provided' { + It 'should use the default ApiVersion' { + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { "6.0" } + + $result = Remove-DevOpsGroup -ApiUri $Uri -GroupDescriptor $GroupDescriptor + + Assert-MockCalled -CommandName Get-AzDevOpsApiVersion -Exactly -Times 1 + $result.success | Should -Be $true + } + } + + Context 'When an error occurs during the API call' { + BeforeAll { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + throw "API call failed" + } + + Mock -CommandName Write-Error -Verifiable + } + + It 'should catch and log the error' { + { Remove-DevOpsGroup -ApiUri $Uri -GroupDescriptor $GroupDescriptor } | Should -Not -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Set-DevOpsGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Set-DevOpsGroup.tests.ps1 new file mode 100644 index 000000000..caa207b32 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Group/Set-DevOpsGroup.tests.ps1 @@ -0,0 +1,63 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-DevOpsGroup' -Tags "Unit", "API" { + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-DevOpsGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '6.0-preview.1' } + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { return [PSCustomObject]@{displayName = 'MyGroup'; description = 'Updated group description'} } + + } + + Context 'Default ParameterSet' { + It 'Should invoke the API with correct parameters and update the group' { + + $params = @{ + ApiUri = "https://dev.azure.com/contoso" + GroupName = "MyGroup" + GroupDescription = "Updated group description" + GroupDescriptor = "some-group-descriptor" + } + + $result = Set-DevOpsGroup @params + + $result.displayName | Should -Be 'MyGroup' + $result.description | Should -Be 'Updated group description' + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 1 + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 -Scope It -ParameterFilter { + $ApiUri -eq "https://dev.azure.com/contoso/_apis/graph/groups/some-group-descriptor?api-version=6.0-preview.1" -and + $Method -eq 'Patch' + } + } + } + + Context 'ProjectScope ParameterSet' { + It 'Should invoke the API with correct parameters and update the group within project scope' { + $params = @{ + ApiUri = "https://dev.azure.com/contoso" + GroupName = "MyGroup" + GroupDescription = "Updated group description" + ProjectScopeDescriptor = "some-project-scope" + } + $result = Set-DevOpsGroup @params + + $result.displayName | Should -Be 'MyGroup' + $result.description | Should -Be 'Updated group description' + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 1 + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 -Scope It -ParameterFilter { + $ApiUri -eq "https://dev.azure.com/contoso/_apis/graph/groups?scopeDescriptor=some-project-scope&api-version=6.0-preview.1" -and + $Method -eq 'Patch' + } + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/New-DevOpsGroupMember.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/New-DevOpsGroupMember.tests.ps1 new file mode 100644 index 000000000..1ada5c190 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/New-DevOpsGroupMember.tests.ps1 @@ -0,0 +1,91 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "New-DevOpsGroupMember Tests" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-DevOpsGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return "3.0-preview" } + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + success = $true + message = "Member added successfully" + } + } + Mock -CommandName Write-Verbose + Mock -CommandName Write-Error + } + + Context "When adding a new member to a DevOps group" { + + It "should call Get-AzDevOpsApiVersion if ApiVersion is not provided" { + $GroupIdentity = [PSCustomObject]@{ descriptor = "group-descriptor" } + $MemberIdentity = [PSCustomObject]@{ descriptor = "member-descriptor" } + $ApiUri = "https://dev.azure.com/organization" + + New-DevOpsGroupMember -GroupIdentity $GroupIdentity -MemberIdentity $MemberIdentity -ApiUri $ApiUri + + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 1 + } + + It "should not call Get-AzDevOpsApiVersion if ApiVersion is provided" { + $GroupIdentity = [PSCustomObject]@{ descriptor = "group-descriptor" } + $MemberIdentity = [PSCustomObject]@{ descriptor = "member-descriptor" } + $ApiVersion = "6.0" + $ApiUri = "https://dev.azure.com/organization" + + New-DevOpsGroupMember -GroupIdentity $GroupIdentity -MemberIdentity $MemberIdentity -ApiUri $ApiUri -ApiVersion $ApiVersion + + Assert-MockCalled Get-AzDevOpsApiVersion -Exactly 0 + } + + It "should call Invoke-AzDevOpsApiRestMethod with correct parameters" { + $GroupIdentity = [PSCustomObject]@{ descriptor = "group-descriptor" } + $MemberIdentity = [PSCustomObject]@{ descriptor = "member-descriptor" } + $ApiVersion = "6.0" + $ApiUri = "https://dev.azure.com/organization" + $expectedUri = "$ApiUri/_apis/graph/memberships/$($MemberIdentity.descriptor)/$($GroupIdentity.descriptor)?api-version=$ApiVersion" + + New-DevOpsGroupMember -GroupIdentity $GroupIdentity -MemberIdentity $MemberIdentity -ApiUri $ApiUri -ApiVersion $ApiVersion + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly 1 -ParameterFilter { + $ApiUri -eq $expectedUri -and + $Method -eq "PUT" + } + } + + It "should write a verbose message if member is added successfully" { + $GroupIdentity = [PSCustomObject]@{ descriptor = "group-descriptor" } + $MemberIdentity = [PSCustomObject]@{ descriptor = "member-descriptor" } + $ApiUri = "https://dev.azure.com/organization" + + New-DevOpsGroupMember -GroupIdentity $GroupIdentity -MemberIdentity $MemberIdentity -ApiUri $ApiUri + + Assert-MockCalled Write-Verbose -Times 3 + } + + It "should write an error message if adding a member to the group fails" { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + throw "API call failed" + } + + $GroupIdentity = [PSCustomObject]@{ descriptor = "group-descriptor" } + $MemberIdentity = [PSCustomObject]@{ descriptor = "member-descriptor" } + $ApiUri = "https://dev.azure.com/organization" + + New-DevOpsGroupMember -GroupIdentity $GroupIdentity -MemberIdentity $MemberIdentity -ApiUri $ApiUri + + Assert-MockCalled Write-Error -Exactly 1 + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/Remove-DevOpsGroupMember.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/Remove-DevOpsGroupMember.tests.ps1 new file mode 100644 index 000000000..50dda54ca --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/GroupMember/Remove-DevOpsGroupMember.tests.ps1 @@ -0,0 +1,65 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-DevOpsGroupMember' -Tags "Unit", "API" { + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-DevOpsGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '6.0-preview' } + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { return $null } + } + + Context 'When all parameters are valid' { + It 'Removes a member from the group' { + $group = [PSCustomObject]@{ descriptor = 'group-descriptor' } + $member = [PSCustomObject]@{ descriptor = 'member-descriptor' } + $apiUri = 'https://dev.azure.com/myorg' + + Remove-DevOpsGroupMember -GroupIdentity $group -MemberIdentity $member -ApiUri $apiUri + + Assert-MockCalled Get-AzDevOpsApiVersion -Times 1 + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $ApiUri -eq 'https://dev.azure.com/myorg/_apis/graph/memberships/member-descriptor/group-descriptor?api-version=6.0-preview' -and + $Method -eq 'DELETE' + } -Times 1 + } + } + + Context 'When API call fails' { + It 'Handles the error gracefully' { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { throw 'API call failed' } + Mock -CommandName Write-Error -Verifiable + + $group = [PSCustomObject]@{ descriptor = 'group-descriptor' } + $member = [PSCustomObject]@{ descriptor = 'member-descriptor' } + $apiUri = 'https://dev.azure.com/myorg' + + { Remove-DevOpsGroupMember -GroupIdentity $group -MemberIdentity $member -ApiUri $apiUri } | Should -Not -Throw + } + } + + Context 'When ApiVersion parameter is provided' { + It 'Uses the specified ApiVersion' { + $group = [PSCustomObject]@{ descriptor = 'group-descriptor' } + $member = [PSCustomObject]@{ descriptor = 'member-descriptor' } + $apiUri = 'https://dev.azure.com/myorg' + $apiVersion = '6.0' + + Remove-DevOpsGroupMember -GroupIdentity $group -MemberIdentity $member -ApiUri $apiUri -ApiVersion $apiVersion + + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $ApiUri -eq "https://dev.azure.com/myorg/_apis/graph/memberships/$($member.descriptor)/$($group.descriptor)?api-version=$apiVersion" -and + $Method -eq 'DELETE' + } -Times 1 + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/New-DevOpsProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/New-DevOpsProject.tests.ps1 new file mode 100644 index 000000000..acb8f85f5 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/New-DevOpsProject.tests.ps1 @@ -0,0 +1,89 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'New-DevOpsProject' -Tags "Unit", "API" { + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-DevOpsProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + @{ + id = "1234" + name = "MyProject" + description = "This is a new project" + visibility = "private" + } + } + } + + Context 'Creates a new Azure DevOps project with valid parameters' { + + It 'Should call Invoke-AzDevOpsApiRestMethod with correct parameters and return the project details' { + $organization = "myorg" + $projectName = "MyProject" + $projectDescription = "This is a new project" + $sourceControlType = "Git" + $processTemplateId = "6b724908-ef14-45cf-84f8-768b5384da45" + $visibility = "private" + $apiVersion = "6.0" + + $result = New-DevOpsProject -Organization $organization -ProjectName $projectName -Description $projectDescription -SourceControlType $sourceControlType -ProcessTemplateId $processTemplateId -Visibility $visibility -ApiVersion $apiVersion + + $result | Should -Not -BeNullOrEmpty + $result.name | Should -Be $projectName + $result.description | Should -Be $projectDescription + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Times 1 -Exactly -Scope It -ParameterFilter { + $params = $_ + $APIUri -eq "https://dev.azure.com/$organization/_apis/projects?api-version=$apiVersion" -and + $Method -eq "POST" -and + $Body -ne $null + } + + } + } + + Context 'Handles errors gracefully' { + BeforeEach { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { throw "API call failed" } -Verifiable + Mock -CommandName Write-Error + } + + It 'Should return an error message if API call fails' { + { + New-DevOpsProject -Organization "myorg" -ProjectName "MyProject" -Description "This is a new project" -SourceControlType "Git" -ProcessTemplateId "6b724908-ef14-45cf-84f8-768b5384da45" -Visibility "private" -ApiVersion "6.0" + } | Should -Not -Throw + + } + } + + Context 'Validates parameters correctly' { + + It 'Should validate ProjectName using Test-AzDevOpsProjectName' { + + # Mock the Test-AzDevOpsProjectName function to return $true + Mock -CommandName Test-AzDevOpsProjectName -MockWith { $true } + + $organization = "myorg" + $projectName = "MyProject" + $projectDescription = "This is a new project" + $sourceControlType = "Git" + $processTemplateId = "6b724908-ef14-45cf-84f8-768b5384da45" + $visibility = "private" + $apiVersion = "6.0" + + $result = New-DevOpsProject -Organization $organization -ProjectName $projectName -Description $projectDescription -SourceControlType $sourceControlType -ProcessTemplateId $processTemplateId -Visibility $visibility -ApiVersion $apiVersion + + Assert-MockCalled -CommandName Test-AzDevOpsProjectName -Times 1 + + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Remove-DevOpsProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Remove-DevOpsProject.tests.ps1 new file mode 100644 index 000000000..4e4613f3d --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Remove-DevOpsProject.tests.ps1 @@ -0,0 +1,50 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Remove-DevOpsProject" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-DevOpsProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return "5.1-preview.1" } + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith {} + + } + + Context "When removing a project" { + + It "Should call Invoke-AzDevOpsApiRestMethod with correct parameters" { + $org = "MyOrganization" + $projectId = "MyProject" + $apiVersion = "5.1-preview.1" + + Remove-DevOpsProject -Organization $org -ProjectId $projectId + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly 1 -ParameterFilter { + $ApiUri -eq "https://dev.azure.com/MyOrganization/_apis/projects/MyProject?api-version=5.1-preview.1" -and + $Method -eq "DELETE" + } + } + + } + + Context "When an exception occurs" { + BeforeEach { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { throw "API call failed" } + Mock -CommandName Write-Error -Verifiable + } + + It "Should write an error message" { + { Remove-DevOpsProject -Organization "MyOrganization" -ProjectId "MyProject" } | Should -Not -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Update-DevOpsProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Update-DevOpsProject.tests.ps1 new file mode 100644 index 000000000..0360f554b --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Update-DevOpsProject.tests.ps1 @@ -0,0 +1,105 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Update-DevOpsProject" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Update-DevOpsProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { + return @{ statusCode = 200; content = "Success" } + } + + } + + Context "When all mandatory parameters are provided" { + + It "Should call Invoke-AzDevOpsApiRestMethod with correct parameters" { + + $organization = "TestOrg" + $projectId = "TestProject" + $apiVersion = "6.0" + + $params = @{ + Organization = $organization + ProjectId = $projectId + ApiVersion = $apiVersion + } + + $result = Update-DevOpsProject @params + + # Assert that the mock was called with expected parameters + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly -Scope It -ParameterFilter { + $ApiUri -eq "https://dev.azure.com/TestOrg/_apis/projects/TestProject?api-version=6.0" -and + $Method -eq 'PATCH' + } + + # Assert that the result is as expected + $result.statusCode | Should -Be 200 + } + } + + Context "When optional parameters are provided" { + + It "Should include optional parameters in the request body" { + $organization = "TestOrg" + $projectId = "TestProject" + $projectDescription = "Test Description" + $visibility = "private" + $apiVersion = "6.0" + + $params = @{ + Organization = $organization + ProjectId = $projectId + ProjectDescription = $projectDescription + Visibility = $visibility + ApiVersion = $apiVersion + } + + $result = Update-DevOpsProject @params + + # Assert that the mock was called with expected parameters + Assert-MockCalled Invoke-AzDevOpsApiRestMethod -Exactly -Scope It -ParameterFilter { + $Body -match '"description": "Test Description"' -and + $Body -match '"visibility": "private"' + } + + # Assert that the result is as expected + $result.statusCode | Should -Be 200 + } + } + + Context "When an error occurs during API call" { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod { + throw "API call failed" + } + + It "Should catch the error and write an error message" { + + Mock -CommandName Write-Error -Verifiable + + $organization = "TestOrg" + $projectId = "TestProject" + $apiVersion = "6.0" + + $params = @{ + Organization = $organization + ProjectId = $projectId + ApiVersion = $apiVersion + } + + { Update-DevOpsProject @params } | Should -Not -Throw + + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Wait-DevOpsProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Wait-DevOpsProject.tests.ps1 new file mode 100644 index 000000000..590784c8d --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Project/Wait-DevOpsProject.tests.ps1 @@ -0,0 +1,111 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Wait-DevOpsProject" -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Wait-DevOpsProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ status = 'wellFormed' } + } + + Mock -CommandName Write-Error + + } + + Context "When project is created successfully" { + + It "Should detect the project has been created successfully and exit the loop" { + $organizationName = "TestOrg" + $projectURL = "https://dev.azure.com/TestOrg/TestProject" + $apiVersion = "6.0" + + $params = @{ + OrganizationName = $organizationName + ProjectURL = $projectURL + ApiVersion = $apiVersion + } + + { Wait-DevOpsProject @params } | Should -Not -Throw + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Time 1 + } + } + + Context "When project creation fails" { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ status = 'failed'; message = 'Creation failed' } + } + + It "Should detect the failure and write an error message" { + $organizationName = "TestOrg" + $projectURL = "https://dev.azure.com/TestOrg/TestProject" + $apiVersion = "6.0" + + $params = @{ + OrganizationName = $organizationName + ProjectURL = $projectURL + ApiVersion = $apiVersion + } + + { Wait-DevOpsProject @params } | Should -Not -Throw + + Assert-MockCalled -CommandName Write-Error + } + } + + Context "When project creation times out" { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ status = 'creating' } + } + + It "Should time out after 10 attempts and write an error message" { + $organizationName = "TestOrg" + $projectURL = "https://dev.azure.com/TestOrg/TestProject" + $apiVersion = "6.0" + + $params = @{ + OrganizationName = $organizationName + ProjectURL = $projectURL + ApiVersion = $apiVersion + } + + { Wait-DevOpsProject @params } | Should -Not -Throw + + Assert-MockCalled -CommandName Write-Error + } + } + + Context "When project creation status is not set" { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ status = 'notSet'; message = 'Status not set' } + } + + It "Should detect the status is not set and write an error message" { + + $organizationName = "TestOrg" + $projectURL = "https://dev.azure.com/TestOrg/TestProject" + $apiVersion = "6.0" + + $params = @{ + OrganizationName = $organizationName + ProjectURL = $projectURL + ApiVersion = $apiVersion + } + + { Wait-DevOpsProject @params } | Should -Not -Throw + + Assert-MockCalled -CommandName Write-Error + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Get-ProjectServiceStatus.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Get-ProjectServiceStatus.tests.ps1 new file mode 100644 index 000000000..0c493f191 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Get-ProjectServiceStatus.tests.ps1 @@ -0,0 +1,83 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-ProjectServiceStatus' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-ProjectServiceStatus.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { + return '6.0-preview.1' + } + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return [pscustomobject]@{ + state = 'enabled' + } + } + + } + + Context 'When all parameters are valid' { + It 'Should return the state of the service as enabled' { + $organization = 'TestOrg' + $projectId = 'TestProjectId' + $serviceName = 'TestServiceName' + + $result = Get-ProjectServiceStatus -Organization $organization -ProjectId $projectId -ServiceName $serviceName + + $result.state | Should -Be 'enabled' + } + + It 'Should call Invoke-AzDevOpsApiRestMethod with correct parameters' { + $organization = 'TestOrg' + $projectId = 'TestProjectId' + $serviceName = 'TestServiceName' + + $result = Get-ProjectServiceStatus -Organization $organization -ProjectId $projectId -ServiceName $serviceName + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Exactly -Times 1 + } + } + + Context 'When service state is undefined' { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return [pscustomobject]@{ + state = 'undefined' + } + } + + It 'Should treat undefined state as enabled' { + $organization = 'TestOrg' + $projectId = 'TestProjectId' + $serviceName = 'TestServiceName' + + $result = Get-ProjectServiceStatus -Organization $organization -ProjectId $projectId -ServiceName $serviceName + + $result.state | Should -Be 'enabled' + } + } + + Context 'When an error occurs during API call' { + + It 'Should write an error message' { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { throw "API Error" } + Mock -CommandName Write-Error -Verifiable + + $organization = 'TestOrg' + $projectId = 'TestProjectId' + $serviceName = 'TestServiceName' + + { Get-ProjectServiceStatus -Organization $organization -ProjectId $projectId -ServiceName $serviceName } | Should -Not -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Set-ProjectServiceStatus.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Set-ProjectServiceStatus.tests.ps1 new file mode 100644 index 000000000..af6753817 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/ProjectServices/Set-ProjectServiceStatus.tests.ps1 @@ -0,0 +1,80 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-ProjectServiceStatus' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-ProjectServiceStatus.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { + return '6.0' + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + state = 'Enabled' + } + } + + } + + It 'should call Invoke-AzDevOpsApiRestMethod with correct parameters' { + + $Organization = 'TestOrg' + $ProjectId = 'TestProjId' + $ServiceName = 'Git' + $Body = @{ + state = 'Enabled' + } + $ApiVersion = '6.0' + + + $expectedUri = 'https://dev.azure.com/TestOrg/_apis/FeatureManagement/FeatureStates/host/project/TestProjId/Git?api-version=6.0' + + Set-ProjectServiceStatus -Organization $Organization -ProjectId $ProjectId -ServiceName $ServiceName -Body $Body -ApiVersion $ApiVersion + + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $Uri -eq $expectedUri -and + $Method -eq 'PATCH' + } -Exactly -Times 1 + } + + It 'should return the state of the service if the API call is successful' { + $Organization = 'TestOrg' + $ProjectId = 'TestProjId' + $ServiceName = 'Git' + $Body = @{ + state = 'Enabled' + } + + $result = Set-ProjectServiceStatus -Organization $Organization -ProjectId $ProjectId -ServiceName $ServiceName -Body $Body + + $result | Should -Be 'Enabled' + } + + It 'should return error message when API call fails' { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + throw "API call failed" + } + + Mock -CommandName Write-Error -Verifiable + + $Organization = 'TestOrg' + $ProjectId = 'TestProjId' + $ServiceName = 'Git' + $Body = @{ + state = 'Enabled' + } + + { Set-ProjectServiceStatus -Organization $Organization -ProjectId $ProjectId -ServiceName $ServiceName -Body $Body } | Should -Not -Throw + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/SecurityDescriptor/Get-DevOpsSecurityDescriptor.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/SecurityDescriptor/Get-DevOpsSecurityDescriptor.tests.ps1 new file mode 100644 index 000000000..257ae5915 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/SecurityDescriptor/Get-DevOpsSecurityDescriptor.tests.ps1 @@ -0,0 +1,52 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-DevOpsSecurityDescriptor Tests' -Tags "Unit", "API" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-DevOpsSecurityDescriptor.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ value = 'MockedResponse' } + } + + $ProjectId = 'TestProjectId' + $Organization = 'TestOrganization' + $ApiVersion = '6.0' + + } + + It 'should retrieve the security descriptor for a project' { + $response = Get-DevOpsSecurityDescriptor -ProjectId $ProjectId -Organization $Organization -ApiVersion $ApiVersion + $response | Should -Be 'MockedResponse' + } + + It 'should call Invoke-AzDevOpsApiRestMethod once' { + Get-DevOpsSecurityDescriptor -ProjectId $ProjectId -Organization $Organization -ApiVersion $ApiVersion + Assert-MockCalled -CommandName 'Invoke-AzDevOpsApiRestMethod' -Exactly 1 -Scope It + } + + It 'should call Invoke-AzDevOpsApiRestMethod with correct parameters' { + Get-DevOpsSecurityDescriptor -ProjectId $ProjectId -Organization $Organization -ApiVersion $ApiVersion + Assert-MockCalled -CommandName 'Invoke-AzDevOpsApiRestMethod' -Exactly 1 -Scope It -ParameterFilter { + $ApiUri -eq "https://vssps.dev.azure.com/TestOrganization/_apis/graph/descriptors/TestProjectId?api-version=6.0" -and + $Method -eq 'GET' + } + } + + It 'should handle errors gracefully' { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { throw "API Error" } + Mock -CommandName Write-Error -Verifiable + + { Get-DevOpsSecurityDescriptor -ProjectId $ProjectId -Organization $Organization -ApiVersion $ApiVersion } | Should -Not -Throw + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Add-AuthenticationHTTPHeader.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Add-AuthenticationHTTPHeader.tests.ps1 new file mode 100644 index 000000000..4f8ff6608 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Add-AuthenticationHTTPHeader.tests.ps1 @@ -0,0 +1,87 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Add-AuthenticationHTTPHeader" -Tags "Unit", "Authentication" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Add-AuthenticationHTTPHeader.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Update-AzManagedIdentity + + } + + BeforeEach { + # Reset the global variables before each test + $Global:DSCAZDO_AuthenticationToken = $null + $Global:DSCAZDO_OrganizationName = "TestOrg" + } + + It "Throws an error when the token is null" { + $Global:DSCAZDO_AuthenticationToken = @{ + tokenType = $null + } + { Add-AuthenticationHTTPHeader } | Should -Throw '*The authentication token is null*' + } + + It "Returns header for PersonalAccessToken" { + $Global:DSCAZDO_AuthenticationToken = [PSCustomObject]@{ + tokenType = 'PersonalAccessToken' + } + $Global:DSCAZDO_AuthenticationToken | Add-Member -MemberType ScriptMethod -Name Get -Value { return "dummyPAT" } + + $result = Add-AuthenticationHTTPHeader + $result | Should -Be "Authorization: Basic dummyPAT" + } + + It "Returns header for ManagedIdentity when token is not expired" { + $Global:DSCAZDO_AuthenticationToken = @{ + tokenType = 'ManagedIdentity' + } + $Global:DSCAZDO_AuthenticationToken | Add-Member -MemberType ScriptMethod -Name Get -Value { return "dummyPAT" } + $Global:DSCAZDO_AuthenticationToken | Add-Member -MemberType ScriptMethod -Name isExpired -Value { return $false } + + $result = Add-AuthenticationHTTPHeader + $result | Should -Be "Bearer dummyPAT" + } + + It "Updates and returns header for ManagedIdentity when token is expired" { + $Global:DSCAZDO_AuthenticationToken = @{ + tokenType = 'ManagedIdentity' + } + $Global:DSCAZDO_AuthenticationToken | Add-Member -MemberType ScriptMethod -Name Get -Value { return "dummyPAT" } + $Global:DSCAZDO_AuthenticationToken | Add-Member -MemberType ScriptMethod -Name isExpired -Value { return $true } + + + # Mock Update-AzManagedIdentity cmdlet + Mock -CommandName Update-AzManagedIdentity -MockWith { + $obj = [PSCustomObject]@{ + tokenType = 'ManagedIdentity' + } + $obj | Add-Member -MemberType ScriptMethod -Name Get -Value { return "newMIToken" } + $obj | Add-Member -MemberType ScriptMethod -Name isExpired -Value { return $false } + + return $obj + } + + $result = Add-AuthenticationHTTPHeader + $result | Should -Be "Bearer newMIToken" + } + + It "Throws an error for unsupported token type" { + $Global:DSCAZDO_AuthenticationToken = @{ + tokenType = 'UnsupportedToken' + Get = { return "dummyToken" } + } + { Add-AuthenticationHTTPHeader } | Should -Throw '*The authentication token type is not supported*' + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-AzManagedIdentityToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-AzManagedIdentityToken.tests.ps1 new file mode 100644 index 000000000..ce7009dda --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Get-AzManagedIdentityToken.tests.ps1 @@ -0,0 +1,209 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Get-AzManagedIdentityToken Tests" -Tags "Unit", "Authentication" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzManagedIdentityToken.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Import the enums + Import-Enums | ForEach-Object { + . $_.FullName + } + + # Import the classes + . (Get-ClassFilePath '001.AuthenticationToken') + . (Get-ClassFilePath '002.PersonalAccessToken') + . (Get-ClassFilePath '003.ManagedIdentityToken') + + + Mock -CommandName Test-AzDevOpsApiHttpRequestHeader -MockWith { + return $true + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { + return "6.0-preview.1" + } + + Mock -CommandName New-ManagedIdentityToken -MockWith { + return @{ + AccessToken = "fake-access-token" + Expiry = (Get-Date).AddHours(1) + } + } + + Mock -CommandName Test-AzToken -MockWith { return $true } + Mock -CommandName Get-OperatingSystemInfo -MockWith { return @{ Windows = $true; Linux = $false; MacOS = $false } } + Mock -CommandName Get-Content -MockWith { return "mock-data" } + Mock -CommandName Test-isWindowsAdmin -MockWith { return $true } + + class CustomException : Exception { + [hashtable] $response + + CustomException($Message, $response) : base($Message) { + $this.response = $response + } + } + + + function Invoke-MockError { + # Create a custom WebException with a response containing headers + $response = @{ + Headers = @{ + wwwAuthenticate = 'Basic realm=MOCKMOCKMOCKMOCKMOCKMOCK' + } + } + + throw [CustomException]::New("Mock Error", $response) + } + + + } + + Context "When Verify switch is not set" { + + BeforeAll { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + access_token = "fake-access-token" + } + } + } + + It "should return managed identity token" { + $result = Get-AzManagedIdentityToken -OrganizationName "Contoso" + $result.AccessToken | Should -Be "fake-access-token" + } + } + + Context "When it is an Azure Arc Machine and the console is not being run as Administrator" { + + AfterAll { + Remove-Variable -Name IDENTITY_ENDPOINT -Scope Global -ErrorAction SilentlyContinue + } + + BeforeAll { + + $env:IDENTITY_ENDPOINT = 'mock-url' + + Mock -CommandName Test-isWindowsAdmin -MockWith { + return $false + } + } + + It "should throw error" { + { + Get-AzManagedIdentityToken -OrganizationName "Contoso" + } | Should -Throw "*Error: Authentication to Azure Arc requires Administrator privileges.*" + } + } + + Context "When it is an Azure Arc Machine and the console is being run as Administrator" { + + AfterAll { + Remove-Variable -Name IDENTITY_ENDPOINT -Scope Global -ErrorAction SilentlyContinue + } + + BeforeAll { + + $env:IDENTITY_ENDPOINT = 'mock-url' + + Mock -CommandName Test-isWindowsAdmin -MockWith { + return $true + } + + } + + It "should not throw error" { + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $null -eq $HttpHeaders.Authorization + } -MockWith { Invoke-MockError } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $null -ne $HttpHeaders.Authorization + } -MockWith { return @{ access_token = "mock-data" } } + + Get-AzManagedIdentityToken -OrganizationName "Contoso" + Assert-MockCalled -CommandName Test-isWindowsAdmin + Assert-MockCalled -CommandName Get-Content + Assert-MockCalled -CommandName Invoke-AzDevOpsApiRestMethod -Times 2 + + } + } + + Context "Linux Machines" { + + BeforeAll { + Mock -CommandName Test-isWindowsAdmin -MockWith { + return $false + } + Mock -CommandName Get-OperatingSystemInfo -MockWith { return @{ Windows = $false; Linux = $true; MacOS = $false } } + Mock -CommandName Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $null -eq $HttpHeaders.Authorization + } -MockWith { Invoke-MockError } + + Mock -CommandName Invoke-AzDevOpsApiRestMethod -ParameterFilter { + $null -ne $HttpHeaders.Authorization + } -MockWith { return @{ access_token = "mock-data" } } + } + + It "should not call Test-isWindowsAdmin" { + Get-AzManagedIdentityToken -OrganizationName "Contoso" + Assert-MockCalled -CommandName Test-isWindowsAdmin -Times 0 + } + } + + Context "When Verify switch is set" { + + BeforeAll { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return @{ + access_token = "fake-access-token" + } + } + } + + It "should return managed identity token after verification" { + $result = Get-AzManagedIdentityToken -OrganizationName "Contoso" -Verify + $result.AccessToken | Should -Be "fake-access-token" + } + + It "should throw error if Token verification fails" { + Mock -CommandName Test-AzToken -MockWith { + return $false + } -ParameterFilter { + $true + } + + { + Get-AzManagedIdentityToken -OrganizationName "Contoso" -Verify + } | Should -Throw "Error. Failed to call the Azure DevOps API." + } + } + + Context "When access token is not returned from Azure Instance Metadata Service" { + + BeforeAll { + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + Throw "MOCK ERROR" + } + } + + It "should throw error" { + { + Get-AzManagedIdentityToken -OrganizationName "Contoso" + } | Should -Throw + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Update-AzManagedIdentity.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Update-AzManagedIdentity.tests.ps1 new file mode 100644 index 000000000..dfe1c9d09 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/ManagedIdentity/Update-AzManagedIdentity.tests.ps1 @@ -0,0 +1,65 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Update-AzManagedIdentity" -Tags "Unit", "Authentication" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Update-AzManagedIdentity.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Import the enums + Import-Enums | ForEach-Object { + . $_.FullName + } + + # Import the classes + . (Get-ClassFilePath '001.AuthenticationToken') + . (Get-ClassFilePath '002.PersonalAccessToken') + . (Get-ClassFilePath '003.ManagedIdentityToken') + + + Mock -CommandName Get-AzManagedIdentityToken -MockWith {} + + } + + Context "When the Global Organization Name is not set" { + It "Throws an error" { + $Global:DSCAZDO_OrganizationName = $null + { Update-AzManagedIdentity } | Should -Throw '*Organization Name is not set*' + } + } + + Context "When the Global Organization Name is set" { + BeforeEach { + $Global:DSCAZDO_OrganizationName = "Contoso" + $Global:DSCAZDO_AuthenticationToken = "oldToken" + } + + It "Clears the existing token" { + Update-AzManagedIdentity + $Global:DSCAZDO_AuthenticationToken | Should -BeNullOrEmpty + } + + It "Calls Get-AzManagedIdentityToken with the correct organization name" { + Mock -CommandName Get-AzManagedIdentityToken -MockWith { return "newToken" } + + Update-AzManagedIdentity + Assert-MockCalled -CommandName Get-AzManagedIdentityToken -Times 1 -Exactly -ParameterFilter { $OrganizationName -eq "Contoso" } + } + + It "Sets the Global Authentication Token to the new token" { + Mock -CommandName Get-AzManagedIdentityToken -MockWith { return "newToken" } + + Update-AzManagedIdentity + $Global:DSCAZDO_AuthenticationToken | Should -Be "newToken" + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/PersonalAccessToken/Set-AzPersonalAccessToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/PersonalAccessToken/Set-AzPersonalAccessToken.tests.ps1 new file mode 100644 index 000000000..aa64d4451 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/PersonalAccessToken/Set-AzPersonalAccessToken.tests.ps1 @@ -0,0 +1,109 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Set-AzPersonalAccessToken" -Tags "Unit", "Authentication" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzPersonalAccessToken.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Import the enums + Import-Enums | ForEach-Object { + . $_.FullName + } + + # Import the classes + . (Get-ClassFilePath '001.AuthenticationToken') + . (Get-ClassFilePath '002.PersonalAccessToken') + . (Get-ClassFilePath '003.ManagedIdentityToken') + + Mock -CommandName New-PersonalAccessToken -MockWith { return } + Mock -CommandName Test-AzToken -MockWith { return $true } + + } + + Context "with PersonalAccessToken parameter set" { + It "should call New-PersonalAccessToken with the correct arguments" { + $token = "testToken" + $orgName = "testOrg" + + Set-AzPersonalAccessToken -OrganizationName $orgName -PersonalAccessToken $token + + Assert-MockCalled -CommandName New-PersonalAccessToken -Exactly 1 -ParameterFilter { + $PersonalAccessToken -eq $token + } + } + + It "should return the token when Verify switch is not set" { + $token = "testToken" + Mock -CommandName New-PersonalAccessToken -MockWith { return $token } + + $result = Set-AzPersonalAccessToken -OrganizationName "testOrg" -PersonalAccessToken $token + $result | Should -Be $token + } + + It "should verify the connection when Verify switch is set" { + $pattoken = "testToken" + $orgName = "testOrg" + + Mock -CommandName Test-AzToken -MockWith { return $true } + Mock -CommandName New-PersonalAccessToken -MockWith { return 'testToken' } + + $result = Set-AzPersonalAccessToken -OrganizationName $orgName -PersonalAccessToken $pattoken -Verify + + Assert-MockCalled -CommandName Test-AzToken -Exactly 1 -ParameterFilter { + $pattoken -eq $token + } + $result | Should -Be $pattoken + } + } + + Context "with SecureStringPersonalAccessToken parameter set" { + It "should call New-PersonalAccessToken with the correct arguments" { + + Mock -CommandName Test-AzToken -MockWith { return $true } + Mock -CommandName New-PersonalAccessToken -MockWith { + return (ConvertTo-SecureString -String "secureTestToken" -AsPlainText -Force) + } + + $secureToken = ConvertTo-SecureString -String "secureTestToken" -AsPlainText -Force + $orgName = "testOrg" + + $result = Set-AzPersonalAccessToken -OrganizationName $orgName -SecureStringPersonalAccessToken $secureToken + Assert-MockCalled -CommandName New-PersonalAccessToken -Exactly 1 -ParameterFilter { + $SecureStringPersonalAccessToken -ne $null + } + $result | Should -BeOfType [SecureString] + + } + + It "should verify the connection when Verify switch is set" { + $secureToken = ConvertTo-SecureString -String "secureTestToken" -AsPlainText -Force + $orgName = "testOrg" + + Mock -CommandName Test-AzToken -MockWith { return $true } + Mock -CommandName New-PersonalAccessToken -MockWith { + return (ConvertTo-SecureString -String "secureTestToken" -AsPlainText -Force) + } + + $result = Set-AzPersonalAccessToken -OrganizationName $orgName -SecureStringPersonalAccessToken $secureToken -Verify + + Assert-MockCalled -CommandName New-PersonalAccessToken -Exactly 1 -ParameterFilter { + $SecureStringPersonalAccessToken -ne $null + } + + Assert-MockCalled -CommandName Test-AzToken -Exactly 1 + + $result | Should -BeOfType [SecureString] + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Test-AzToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Test-AzToken.tests.ps1 new file mode 100644 index 000000000..20f3bc4d9 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Test-AzToken.tests.ps1 @@ -0,0 +1,65 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Test-AzToken' -Tags "Unit", "Authentication" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzToken.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Test-AzDevOpsApiHttpRequestHeader -MockWith { + return $true + } + + $GLOBAL:DSCAZDO_OrganizationName = "TestOrg" + + } + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + Context 'When token is valid' { + It 'Should return true' { + # Mocking the Managed Identity token object + $mockToken = [PSCustomObject]@{} + $mockToken | Add-Member -MemberType ScriptMethod -Name Get -Value { + return "valid_token" + } + + # Mocking the Invoke-AzDevOpsApiRestMethod cmdlet + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + return "valid_token" + } + + $result = Test-AzToken -Token $mockToken + $result | Should -Be $true + } + } + + Context 'When token is invalid' { + It 'Should return false' { + # Mocking the Managed Identity token object + $mockToken = [PSCustomObject]@{} + $mockToken | Add-Member -MemberType ScriptMethod -Name Get -Value { + return "invalid_token" + } + + # Mocking the Invoke-AzDevOpsApiRestMethod cmdlet to throw an exception + Mock -CommandName Invoke-AzDevOpsApiRestMethod -MockWith { + throw "Unauthorized" + } + + $result = Test-AzToken -Token $mockToken + $result | Should -Be $false + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Add-CacheItem.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Add-CacheItem.tests.ps1 new file mode 100644 index 000000000..6dfad5c14 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Add-CacheItem.tests.ps1 @@ -0,0 +1,129 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Add-CacheItem" -Tags "Unit", "Cache" { + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Add-CacheItem.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + # Mock dependencies + Mock -CommandName Get-CacheObject -MockWith { return @() } + + } + + Context "when adding a new cache item" { + + BeforeEach { + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + } + + It "should retrieve the current cache" { + Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' + + Assert-MockCalled -CommandName Get-CacheObject -Exactly 1 -ParameterFilter { + $CacheType -eq 'Project' + } + } + + It "should create a new cache if the current cache is empty" { + Mock -CommandName Get-CacheObject -MockWith { return @() } + Mock -CommandName Write-Verbose + Mock -CommandName Set-Variable -Verifiable + + Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' + + Assert-MockCalled -CommandName Write-Verbose -Exactly 1 -ParameterFilter { + $Message -eq '[Add-CacheItem] Cache is empty. Creating new cache.' + } + + } + + It "should add a new cache item with the correct key and value" { + Mock -CommandName Get-CacheObject -MockWith { return @() } + Mock -CommandName Write-Verbose + + Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' + + $cache = Get-Variable -Name "AzDoProject" -Scope Global -ValueOnly + $cache[0].Key | Should -Be 'MyKey' + $cache[0].Value | Should -Be 'MyValue' + } + } + + Context "when the cache already contains the key" { + + BeforeEach { + Mock -CommandName Get-CacheObject -MockWith { + return $Global:AzDoProject + } + } + + AfterEach { + Remove-Variable -Name "AzDoProject" -Scope Global + } + + It "should remove the existing cache item" { + Mock -CommandName Write-Verbose + Mock -CommandName Write-Warning + Mock -CommandName Remove-CacheItem -Verifiable -MockWith { + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + } + + Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' + Add-CacheItem -Key 'MyKey' -Value 'NewValue' -Type 'Project' + + $cache = Get-Variable -Name "AzDoProject" -Scope Global -ValueOnly + $cache[0].Key | Should -Be 'MyKey' + $cache[0].Value | Should -Be 'NewValue' + + } + + It "should add the new cache item after removing the old one" { + Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' + + $cache = Get-Variable -Name "AzDoProject" -Scope Global -ValueOnly + $cache[0].Key | Should -Be 'MyKey' + $cache[0].Value | Should -Be 'MyValue' + } + + It "should suppress the warning if SuppressWarning switch is present" { + Mock -CommandName Write-Warning + Mock -CommandName Write-Verbose + + Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' -SuppressWarning + Add-CacheItem -Key 'MyKey' -Value 'NewValue' -Type 'Project' -SuppressWarning + + Assert-MockCalled -CommandName Write-Warning -Exactly 0 + Assert-MockCalled -CommandName Write-Verbose -Times 3 + + } + + It "should display a warning if SuppressWarning switch is not present" { + Mock -CommandName Write-Warning + + Add-CacheItem -Key 'MyKey' -Value 'MyValue' -Type 'Project' + Add-CacheItem -Key 'MyKey' -Value 'NewValue' -Type 'Project' + + Assert-MockCalled -CommandName Write-Warning -Exactly 1 + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/0.ProjectCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/0.ProjectCache.tests.ps1 new file mode 100644 index 000000000..076e1f07f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/0.ProjectCache.tests.ps1 @@ -0,0 +1,108 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'AzDoAPI_0_ProjectCache' -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '0.ProjectCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName List-DevOpsProjects -MockWith { + param ($Organization) + return @( + [pscustomobject]@{ Id = 1; Name = 'Project1' }, + [pscustomobject]@{ Id = 2; Name = 'Project2' } + ) + } + + Mock -CommandName Get-DevOpsSecurityDescriptor -MockWith { + param ($ProjectId, $Organization) + return "SecurityDescriptor for Project $ProjectId" + } + + Mock -CommandName Add-CacheItem -MockWith { + param ($Key, $Value, $Type) + } + + Mock -CommandName Export-CacheObject -MockWith { + param ($CacheType, $Content) + } + + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '6.0' } + + } + + Context 'When OrganizationName is provided' { + + It 'should call List-DevOpsProjects with the correct parameters' { + AzDoAPI_0_ProjectCache -OrganizationName 'MyOrganization' + + Assert-MockCalled List-DevOpsProjects -Exactly -Times 1 + } + + It 'should add projects to the cache' { + AzDoAPI_0_ProjectCache -OrganizationName 'MyOrganization' + + Assert-MockCalled Add-CacheItem -Exactly -Times 2 -ParameterFilter { + ($Key -eq 'Project1' -and $Value.Name -eq 'Project1') -or + ($Key -eq 'Project2' -and $Value.Name -eq 'Project2') + } + } + + It 'should export the cache' { + AzDoAPI_0_ProjectCache -OrganizationName 'MyOrganization' + + Assert-MockCalled Export-CacheObject -Exactly -Times 1 -ParameterFilter { + ($CacheType -eq 'LiveProjects') -and + ($Content -eq $global:AzDoLiveProjects) + } + } + } + + Context 'When OrganizationName is not provided' { + + Mock -CommandName Write-Verbose -MockWith { + param ($Message) + } + + BeforeAll { + $Global:DSCAZDO_OrganizationName = 'GlobalOrganization' + } + + It 'should use the global variable for organization name' { + AzDoAPI_0_ProjectCache + + Assert-MockCalled List-DevOpsProjects -Exactly -Times 1 + } + } + + Context 'Error handling' { + + It 'should handle errors during API call' { + Mock -CommandName List-DevOpsProjects -MockWith { throw "API Error" } + Mock -CommandName Write-Error -Verifiable + + { AzDoAPI_0_ProjectCache -OrganizationName 'MyOrganization' } | Should -Not -Throw + Assert-VerifiableMock + } + + It 'should handle errors during cache export' { + Mock -CommandName Export-CacheObject -MockWith { throw "Export failed" } + Mock -CommandName Write-Error -Verifiable + + { AzDoAPI_0_ProjectCache -OrganizationName 'MyOrganization' } | Should -Not -Throw + Assert-VerifiableMock + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/1.GroupCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/1.GroupCache.tests.ps1 new file mode 100644 index 000000000..3c9e83417 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/1.GroupCache.tests.ps1 @@ -0,0 +1,105 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'AzDoAPI_1_GroupCache' -Tags "Unit", "Cache" { + + AfterAll { + Remove-Variable -Name "AzDoProject" -Scope Global -ErrorAction SilentlyContinue + Remove-Variable -Name "AzDoLiveGroups" -Scope Global -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the Organization + $null = Remove-Variable -Name "DSCAZDO_OrganizationName" -Scope Global -ErrorAction SilentlyContinue + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '1.GroupCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName List-DevOpsGroups -MockWith { + return @( + [pscustomobject]@{ PrincipalName = 'Group1'; Id = 1 }, + [pscustomobject]@{ PrincipalName = 'Group2'; Id = 2 } + ) + } + + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + } + + Context 'When OrganizationName is provided' { + + It 'should call List-DevOpsGroups with the correct parameters' { + AzDoAPI_1_GroupCache -OrganizationName 'MyOrganization' + + Assert-MockCalled List-DevOpsGroups -Exactly -Times 1 -Scope It -ParameterFilter { + $Organization -eq 'MyOrganization' + } + } + + It 'should add groups to the cache' { + AzDoAPI_1_GroupCache -OrganizationName 'MyOrganization' + + Assert-MockCalled Add-CacheItem -Exactly -Times 2 -Scope It -ParameterFilter { + ($Key -eq 'Group1' -and $Value.PrincipalName -eq 'Group1') -or + ($Key -eq 'Group2' -and $Value.PrincipalName -eq 'Group2') + } + } + + It 'should export the cache' { + AzDoAPI_1_GroupCache -OrganizationName 'MyOrganization' + + Assert-MockCalled Export-CacheObject -Exactly -Times 1 -Scope It -ParameterFilter { + $CacheType -eq 'LiveGroups' + } + } + } + + Context 'When OrganizationName is not provided' { + + Mock -CommandName Write-Verbose -MockWith { + param ($Message) + } + + BeforeAll { + $Global:DSCAZDO_OrganizationName = 'GlobalOrganization' + } + + It 'should use the global variable for organization name' { + AzDoAPI_1_GroupCache + + Assert-MockCalled List-DevOpsGroups -Exactly -Times 1 -Scope It -ParameterFilter { + $Organization -eq 'GlobalOrganization' + } + } + } + + Context 'Error handling' { + + It 'should handle errors during API call' { + Mock -CommandName List-DevOpsGroups -MockWith { throw "API Error" } + Mock -CommandName Write-Error -Verifiable + + { AzDoAPI_1_GroupCache -OrganizationName 'MyOrganization' } | Should -Not -Throw + Assert-VerifiableMock + } + + It 'should handle errors during cache export' { + Mock -CommandName Export-CacheObject -MockWith { throw "Export failed" } + Mock -CommandName Write-Error -Verifiable + + { AzDoAPI_1_GroupCache -OrganizationName 'MyOrganization' } | Should -Not -Throw + Assert-VerifiableMock + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/2.UserCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/2.UserCache.tests.ps1 new file mode 100644 index 000000000..bdda2538e --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/2.UserCache.tests.ps1 @@ -0,0 +1,84 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'AzDoAPI_2_UserCache' -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '2.UserCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + + Mock -CommandName List-UserCache -MockWith { + return @( + [PSCustomObject]@{ PrincipalName = 'user1@example.com' }, + [PSCustomObject]@{ PrincipalName = 'user2@example.com' } + ) + } + + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + } + + Context 'when organization name parameter is provided' { + + It 'should call List-UserCache with correct parameters' { + AzDoAPI_2_UserCache -OrganizationName 'TestOrg' + Assert-MockCalled -CommandName List-UserCache -Exactly -Times 1 + } + + It 'should add users to cache' { + AzDoAPI_2_UserCache -OrganizationName 'TestOrg' + + Assert-MockCalled -CommandName Add-CacheItem -Exactly -Times 2 + Assert-MockCalled -CommandName Add-CacheItem -ParameterFilter { + $Key -eq 'user1@example.com' + } + Assert-MockCalled -CommandName Add-CacheItem -ParameterFilter { + $Key -eq 'user2@example.com' + } + } + + It 'should export the cache' { + AzDoAPI_2_UserCache -OrganizationName 'TestOrg' + + Assert-MockCalled -CommandName Export-CacheObject -Exactly -Times 1 + } + } + + Context 'when organization name parameter is not provided' { + + BeforeEach { + $Global:DSCAZDO_OrganizationName = 'GlobalTestOrg' + } + + It 'should use the global organization name' { + AzDoAPI_2_UserCache + Assert-MockCalled -CommandName List-UserCache -Exactly -Times 1 + } + } + + Context 'when an error occurs' { + + BeforeAll { + Mock -CommandName Write-Error -Verifiable + Mock -CommandName List-UserCache -MockWith { throw "API Error" } + } + + It 'should catch and handle the error' { + { AzDoAPI_2_UserCache -OrganizationName 'TestOrg' } | Should -Not -Throw + Assert-VerifiableMock + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/3.GroupMemberCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/3.GroupMemberCache.tests.ps1 new file mode 100644 index 000000000..4a5f61dba --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/3.GroupMemberCache.tests.ps1 @@ -0,0 +1,99 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'AzDoAPI_3_GroupMemberCache' -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '3.GroupMemberCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-CacheObject -MockWith { + if ($CacheType -eq 'LiveGroups') + { + return @( + [PSCustomObject]@{ Key = 'Group1'; Value = [PSCustomObject]@{ descriptor = 'desc1'; PrincipalName = 'Group1' } }, + [PSCustomObject]@{ Key = 'Group2'; Value = [PSCustomObject]@{ descriptor = 'desc2'; PrincipalName = 'Group2' } } + ) + } elseif ($CacheType -eq 'LiveUsers') { + return @( + [PSCustomObject]@{ descriptor = 'desc1'; PrincipalName = 'user1@example.com' }, + [PSCustomObject]@{ descriptor = 'desc2'; PrincipalName = 'user2@example.com' } + ) + } + } + + Mock -CommandName List-DevOpsGroupMembers -MockWith { + param ( + [string]$Organization, + [string]$GroupDescriptor + ) + return [PSCustomObject]@{ memberDescriptor = @('desc1', 'desc2') } + } + + Mock -CommandName Add-CacheItem -MockWith {} + Mock -CommandName Export-CacheObject -MockWith {} + + } + + Context 'when organization name parameter is provided' { + + It 'should call List-DevOpsGroupMembers with correct parameters' { + AzDoAPI_3_GroupMemberCache -OrganizationName 'TestOrg' + + Assert-MockCalled -CommandName List-DevOpsGroupMembers -Exactly -Times 2 -Scope It -ParameterFilter { + $Organization -eq 'TestOrg' + } + } + + It 'should add group members to cache' { + AzDoAPI_3_GroupMemberCache -OrganizationName 'TestOrg' + + Assert-MockCalled -CommandName Add-CacheItem -Exactly -Times 2 + } + + It 'should export the cache' { + AzDoAPI_3_GroupMemberCache -OrganizationName 'TestOrg' + + Assert-MockCalled -CommandName Export-CacheObject -Exactly -Times 1 + } + } + + Context 'when organization name parameter is not provided' { + + BeforeEach { + $Global:DSCAZDO_OrganizationName = 'GlobalTestOrg' + } + + It 'should use the global organization name' { + AzDoAPI_3_GroupMemberCache + + Assert-MockCalled -CommandName List-DevOpsGroupMembers -Exactly -Times 2 -ParameterFilter { + $Organization -eq 'GlobalTestOrg' + } + } + } + + Context 'when an error occurs' { + + BeforeAll { + Mock -CommandName List-DevOpsGroupMembers -MockWith { throw "API Error" } + Mock -CommandName Write-Error -Verifiable + } + + It 'should catch and handle the error' { + { AzDoAPI_3_GroupMemberCache -OrganizationName 'TestOrg' } | Should -Not -Throw + Assert-VerifiableMock + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/4.GitRepositoryCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/4.GitRepositoryCache.tests.ps1 new file mode 100644 index 000000000..4d1c3c17c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/4.GitRepositoryCache.tests.ps1 @@ -0,0 +1,109 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "AzDoAPI_4_GitRepositoryCache Tests" -Tags "Unit", "Cache" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '4.GitRepositoryCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-CacheObject + Mock -CommandName List-DevOpsGitRepository + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + } + + Context "When \ is passed" { + It "Should call Get-CacheObject with LiveProjects" { + AzDoAPI_4_GitRepositoryCache -OrganizationName "TestOrg" + Assert-MockCalled -CommandName Get-CacheObject -Exactly -Times 1 -ParameterFilter { $CacheType -eq 'LiveProjects' } + } + + It "Should call List-DevOpsGitRepository for each project" { + $mockProjects = @( + [PSCustomObject]@{ Value = @{ Name = "TestProject1" } }, + [PSCustomObject]@{ Value = @{ Name = "TestProject2" } } + ) + Mock -CommandName Get-CacheObject -MockWith { $mockProjects } + AzDoAPI_4_GitRepositoryCache -OrganizationName "TestOrg" + Assert-MockCalled -CommandName List-DevOpsGitRepository -Exactly -Times 2 + } + + It "Should call Add-CacheItem for each repository" { + $mockProjects = @( + [PSCustomObject]@{ Value = @{ Name = "TestProject1" } }, + [PSCustomObject]@{ Value = @{ Name = "TestProject2" } } + ) + $mockRepos = @( + [PSCustomObject]@{ Name = "Repo1" }, + [PSCustomObject]@{ Name = "Repo2" } + ) + Mock -CommandName Get-CacheObject -MockWith { $mockProjects } + Mock -CommandName List-DevOpsGitRepository -MockWith { $mockRepos } + AzDoAPI_4_GitRepositoryCache -OrganizationName "TestOrg" + Assert-MockCalled -CommandName Add-CacheItem -Exactly -Times 4 + } + + It "Should call Export-CacheObject once" { + AzDoAPI_4_GitRepositoryCache -OrganizationName "TestOrg" + Assert-MockCalled -CommandName Export-CacheObject -Exactly -Times 1 + } + + It "Should log verbose messages" { + $ProgressPreference='SilentlyContinue' + Mock -CommandName Write-Verbose -Verifiable + AzDoAPI_4_GitRepositoryCache -OrganizationName "TestOrg" -Verbose + Assert-VerifiableMock + } + + It "Should catch and log errors" { + + $mockProjects = @( + [PSCustomObject]@{ Value = @{ Name = "TestProject1" } }, + [PSCustomObject]@{ Value = @{ Name = "TestProject2" } } + ) + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName List-DevOpsGitRepository -MockWith { throw "Mocked Error" } + Mock -CommandName Get-CacheObject -MockWith { $mockProjects } + + { AzDoAPI_4_GitRepositoryCache -OrganizationName "TestOrg" } | Should -Not -Throw + + Assert-VerifiableMock + } + + } + + Context "When \ is not passed" { + BeforeAll { $Global:DSCAZDO_OrganizationName = "GlobalOrg" } + + It "Should use global variable for organization name" { + $mockProjects = @( + [PSCustomObject]@{ Value = @{ Name = "TestProject1" } }, + [PSCustomObject]@{ Value = @{ Name = "TestProject2" } } + ) + $mockRepos = @( + [PSCustomObject]@{ Name = "Repo1" }, + [PSCustomObject]@{ Name = "Repo2" } + ) + + Mock -CommandName Get-CacheObject -MockWith { $mockProjects } + Mock -CommandName List-DevOpsGitRepository -MockWith { $mockRepos } + + AzDoAPI_4_GitRepositoryCache + Assert-MockCalled -CommandName List-DevOpsGitRepository -Times 1 -ParameterFilter { $OrganizationName -eq "GlobalOrg" } + + } + + AfterAll { Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/5.PermissionsCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/5.PermissionsCache.tests.ps1 new file mode 100644 index 000000000..d51975541 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/5.PermissionsCache.tests.ps1 @@ -0,0 +1,55 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'AzDoAPI_5_PermissionsCache Tests' -Tags "Unit", "Cache" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '5.PermissionsCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName List-DevOpsSecurityNamespaces -MockWith { + return @( + [PSCustomObject]@{ name = 'Namespace1'; namespaceId = 1; writePermission = $true; readPermission = $true; dataspaceCategory = 'category1'; actions = @('Action1','Action2') }, + [PSCustomObject]@{ name = 'Namespace2'; namespaceId = 2; writePermission = $false; readPermission = $true; dataspaceCategory = 'category2'; actions = @('Action3','Action4') } + ) + } + + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + $global:DSCAZDO_OrganizationName = "DefaultOrg" + + } + + It 'Uses provided OrganizationName parameter' { + AzDoAPI_5_PermissionsCache -OrganizationName 'TestOrg' + Assert-VerifiableMock + } + + It 'Uses global OrganizationName when parameter is not provided' { + AzDoAPI_5_PermissionsCache + Assert-MockCalled -CommandName List-DevOpsSecurityNamespaces -ParameterFilter { $OrganizationName -eq $global:DSCAZDO_OrganizationName } + } + + It 'Adds each security namespace to cache correctly' { + AzDoAPI_5_PermissionsCache -OrganizationName 'TestOrg' + + Assert-MockCalled -CommandName Add-CacheItem -Times 2 + Assert-MockCalled -CommandName Add-CacheItem -ParameterFilter { $Key -eq 'Namespace1' -and $Type -eq 'SecurityNamespaces' } + Assert-MockCalled -CommandName Add-CacheItem -ParameterFilter { $Key -eq 'Namespace2' -and $Type -eq 'SecurityNamespaces' } + } + + It 'Exports cache correctly' { + AzDoAPI_5_PermissionsCache -OrganizationName 'TestOrg' + + Assert-MockCalled -CommandName Export-CacheObject -ParameterFilter { $CacheType -eq 'SecurityNameSpaces' -and $Depth -eq 5 } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/6.ServicePrinciple.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/6.ServicePrinciple.tests.ps1 new file mode 100644 index 000000000..82f86b85a --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/6.ServicePrinciple.tests.ps1 @@ -0,0 +1,74 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'AzDoAPI_6_ServicePrinciple' -Tags "Unit", "Cache" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '6.ServicePrinciple.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName List-DevOpsServicePrinciples -MockWith { + return @([PSCustomObject]@{ displayName = 'SP1' }, [PSCustomObject]@{ displayName = 'SP2' }) + } + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + $Global:DSCAZDO_OrganizationName = 'MyOrganization' + } + + It 'should call List-DevOpsServicePrinciples with provided organization name' { + AzDoAPI_6_ServicePrinciple -OrganizationName 'TestOrgName' + + Assert-MockCalled -CommandName List-DevOpsServicePrinciples -Exactly 1 -ParameterFilter { + $OrganizationName -eq 'TestOrgName' + } + } + + It 'should call List-DevOpsServicePrinciples with global organization name if none provided' { + AzDoAPI_6_ServicePrinciple + + Assert-MockCalled -CommandName List-DevOpsServicePrinciples -Exactly 1 -ParameterFilter { + $OrganizationName -eq $Global:DSCAZDO_OrganizationName + } + } + + It 'should add returned service principals to cache' { + AzDoAPI_6_ServicePrinciple -OrganizationName 'TestOrgName' + + Assert-MockCalled -CommandName Add-CacheItem -Exactly 2 + Assert-MockCalled -CommandName Add-CacheItem -ParameterFilter { + $Key -eq 'SP1' -and + $Value.displayName -eq 'SP1' -and + $Type -eq 'LiveServicePrinciples' + } + Assert-MockCalled -CommandName Add-CacheItem -ParameterFilter { + $Key -eq 'SP2' -and + $Value.displayName -eq 'SP2' -and + $Type -eq 'LiveServicePrinciples' + } + } + + It 'should export cache object after adding service principals' { + AzDoAPI_6_ServicePrinciple -OrganizationName 'TestOrgName' + + Assert-MockCalled -CommandName Export-CacheObject -Exactly 1 -ParameterFilter { $CacheType -eq 'LiveServicePrinciples' } + } + + It 'should write an error message if an exception occurs' { + Mock -CommandName List-DevOpsServicePrinciples -MockWith { throw 'API Error' } + Mock -CommandName Write-Error -Verifiable + + { AzDoAPI_6_ServicePrinciple -OrganizationName 'TestOrgName' } | Should -Not -Throw + + Assert-MockCalled -CommandName Write-Error -Exactly 1 + Assert-VerifiableMock + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/7.IdentitySubjectDescriptors.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/7.IdentitySubjectDescriptors.tests.ps1 new file mode 100644 index 000000000..f76d31fdc --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/7.IdentitySubjectDescriptors.tests.ps1 @@ -0,0 +1,144 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "AzDoAPI_7_IdentitySubjectDescriptors" -Tags "Unit", "Cache" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '7.IdentitySubjectDescriptors.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDoCacheObjects -MockWith { + @('LiveGroups', 'LiveUsers', 'LiveServicePrinciples') + } + + Mock -CommandName Get-CacheObject -MockWith { + + switch ($CacheType) + { + 'LiveGroups' { + return ( + [PSCustomObject]@{ + Key = 'mockKey' + Value = @{ + descriptor = 'mockDescriptorGroup' + } + } + ) + } + 'LiveUsers' { + return ( + [PSCustomObject]@{ + Key = 'mockKey' + Value = @{ + descriptor = 'mockDescriptorUser' + } + } + ) + } + 'LiveServicePrinciples' { + return ( + [PSCustomObject]@{ + Key = 'mockKey' + Value = @{ + descriptor = 'mockDescriptorServicePrinciple' + } + } + ) + } + } + } + + Mock -CommandName Get-DevOpsDescriptorIdentity -MockWith { + @{ + id = 'mockId' + descriptor = 'mockDescriptor' + subjectDescriptor = 'mockSubjectDescriptor' + providerDisplayName = 'mockProvider' + isActive = $true + isContainer = $false + } + } + + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + } + + BeforeEach { + $Global:DSCAZDO_OrganizationName = 'mockOrg' + } + + It "should use a global variable if OrganizationName parameter is not provided" { + + + $result = AzDoAPI_7_IdentitySubjectDescriptors + + Assert-MockCalled -CommandName Get-CacheObject + Assert-MockCalled -CommandName Get-DevOpsDescriptorIdentity + Assert-MockCalled -CommandName Add-CacheItem + Assert-MockCalled -CommandName Export-CacheObject + } + + It "should use OrganizationName parameter if provided" { + $result = AzDoAPI_7_IdentitySubjectDescriptors -OrganizationName 'testOrg' + + Assert-MockCalled -CommandName Get-CacheObject + Assert-MockCalled -CommandName Get-DevOpsDescriptorIdentity + Assert-MockCalled -CommandName Add-CacheItem + Assert-MockCalled -CommandName Export-CacheObject + } + + It "should call Get-CacheObject for each cache type" { + $result = AzDoAPI_7_IdentitySubjectDescriptors -OrganizationName 'testOrg' + Assert-MockCalled -CommandName Get-CacheObject -Times 3 + } + + It "should add members to each cache object" { + + Mock -CommandName Get-DevOpsDescriptorIdentity -MockWith { + @{ + id = 'mockId' + descriptor = 'mockDescriptor' + subjectDescriptor = 'mockSubjectDescriptor' + providerDisplayName = 'mockProvider' + isActive = $true + isContainer = $false + } + } + + $mockGroup = @{ + Key = 'mockKey' + Value = [PSCustomObject]@{ + descriptor = 'mockDescriptorGroup' + } + } + + $result = AzDoAPI_7_IdentitySubjectDescriptors -OrganizationName 'testOrg' + + Assert-MockCalled -CommandName Get-DevOpsDescriptorIdentity -ParameterFilter { + $SubjectDescriptor -eq 'mockDescriptorGroup' + } + + $cacheItemArgs = @{ + Key = 'mockKey' + Value = $mockGroup + Type = 'LiveGroups' + SuppressWarning = $true + } + + Assert-MockCalled -CommandName Add-CacheItem -Times 1 -ParameterFilter { + $Key -eq $cacheItemArgs.Key -and + $Type -eq $cacheItemArgs.Type -and + $SuppressWarning -eq $cacheItemArgs.SuppressWarning + } + + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/8.ProcessTemplates.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/8.ProcessTemplates.tests.ps1 new file mode 100644 index 000000000..331963e53 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Cache Initalization/8.ProcessTemplates.tests.ps1 @@ -0,0 +1,79 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'AzDoAPI_8_ProjectProcessTemplates' -Tags "Unit", "Cache" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath '8.ProcessTemplates.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + $mockOrganizationName = 'TestOrg' + $mockProcesses = @( + [PSCustomObject]@{ name = 'Process1' }, + [PSCustomObject]@{ name = 'Process2' } + ) + + $script:global:DSCAZDO_OrganizationName = 'DefaultOrg' + + Mock -CommandName List-DevOpsProcess -MockWith { + param($param) $mockProcesses + } + + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + + } + + Context 'When OrganizationName parameter is provided' { + It 'should call List-DevOpsProcess with provided OrganizationName' { + AzDoAPI_8_ProjectProcessTemplates -OrganizationName $mockOrganizationName + + Assert-MockCalled List-DevOpsProcess -ParameterFilter { $Organization -eq $mockOrganizationName } -Exactly 1 + } + + It 'should add processes to cache' { + AzDoAPI_8_ProjectProcessTemplates -OrganizationName $mockOrganizationName + + $mockProcesses | ForEach-Object { + Assert-MockCalled Add-CacheItem -ParameterFilter { + $Key -eq $_.name -and + $Value -eq $_ -and + $Type -eq 'LiveProcesses' + } -Exactly 1 + } + } + + It 'should export cache' { + AzDoAPI_8_ProjectProcessTemplates -OrganizationName $mockOrganizationName + + Assert-MockCalled Export-CacheObject -ParameterFilter { $CacheType -eq 'LiveProcesses' } + } + } + + Context 'When OrganizationName parameter is not provided' { + It 'should call List-DevOpsProcess with global OrganizationName' { + AzDoAPI_8_ProjectProcessTemplates + + Assert-MockCalled List-DevOpsProcess -ParameterFilter { $Organization -eq $global:DSCAZDO_OrganizationName } -Exactly 1 + } + } + + Context 'When an error occurs during function execution' { + It 'should catch and handle the error' { + Mock -CommandName List-DevOpsProcess -MockWith { throw 'An error occurred' } + Mock -CommandName Write-Error -Verifiable + + AzDoAPI_8_ProjectProcessTemplates -OrganizationName $mockOrganizationName + + Assert-VerifiableMock + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Export-CacheObject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Export-CacheObject.tests.ps1 new file mode 100644 index 000000000..e583f5bf7 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Export-CacheObject.tests.ps1 @@ -0,0 +1,133 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Export-CacheObject" -Tags "Unit", "Cache" { + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Export-CacheObject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + # Mock dependencies + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('Project', 'Team', 'Group', 'SecurityDescriptor') } -Verifiable + Mock -CommandName Test-Path -MockWith { param ($Path) return $false } -Verifiable + Mock -CommandName New-Item -MockWith { param ($Path, $ItemType) } -Verifiable + Mock -CommandName Export-Clixml -MockWith { param ($InputObject, $Depth, $LiteralPath) } -Verifiable + + } + + BeforeEach { + $env:AZDODSC_CACHE_DIRECTORY = "C:\MockPath\AzDoCache" + } + + AfterEach { + Remove-Variable -Name AZDODSC_CACHE_DIRECTORY -ErrorAction SilentlyContinue + } + + Context "when the environment variable AZDODSC_CACHE_DIRECTORY is not set" { + BeforeEach { + $env:AZDODSC_CACHE_DIRECTORY = $null + } + + It "should throw an error" { + { Export-CacheObject -CacheType 'Project' -Content @() } | Should -Throw "The environment variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the variable to the path of the cache directory." + } + } + + Context "when the environment variable AZDODSC_CACHE_DIRECTORY is set" { + BeforeEach { + $env:AZDODSC_CACHE_DIRECTORY = "C:\Temp\AzDoCache" + } + + It "should create the cache directory if it does not exist" { + Mock -CommandName Test-Path -MockWith { param ($Path) return $false } -Verifiable + Mock -CommandName New-Item -Verifiable + + Export-CacheObject -CacheType 'Project' -Content @() + + Assert-MockCalled -CommandName New-Item -Exactly 1 -ParameterFilter { + $Path -eq "C:\Temp\AzDoCache\Cache" -and + $ItemType -eq "Directory" + } + } + + It "should not create the cache directory if it already exists" { + Mock -CommandName Test-Path -MockWith { param ($Path) return $true } -Verifiable + Mock -CommandName New-Item + + Export-CacheObject -CacheType 'Project' -Content @() + + Assert-MockCalled -CommandName New-Item -Exactly 0 -Scope It + } + + It "should save content to the cache file" { + + Mock -CommandName Export-Clixml -Verifiable + + $content = @( + [PSCustomObject]@{ Name = 'Project1'; Id = 1 }, + [PSCustomObject]@{ Name = 'Project2'; Id = 2 } + ) + + Export-CacheObject -CacheType 'Project' -Content $content + Assert-MockCalled -CommandName Export-Clixml -Times 1 + + } + + It "should use the default depth value of 3" { + + Mock -CommandName Export-Clixml + + $content = @( + [PSCustomObject]@{ Name = 'Project1'; Id = 1 }, + [PSCustomObject]@{ Name = 'Project2'; Id = 2 } + ) + + Export-CacheObject -CacheType 'Project' -Content $content + + Assert-MockCalled -CommandName Export-Clixml -Times 1 -ParameterFilter { + $Depth -eq 3 -and + $LiteralPath -eq "C:\Temp\AzDoCache\Cache\Project.clixml" + } + } + + It "should use the specified depth value" { + + Mock -CommandName Export-Clixml + + $content = @( + [PSCustomObject]@{ Name = 'Project1'; Id = 1 }, + [PSCustomObject]@{ Name = 'Project2'; Id = 2 } + ) + + Export-CacheObject -CacheType 'Project' -Content $content -Depth 5 + + Assert-MockCalled -CommandName Export-Clixml -Times 1 -ParameterFilter { + $Depth -eq 5 -and + $LiteralPath -eq "C:\Temp\AzDoCache\Cache\Project.clixml" + } + } + } + + Context "when invalid CacheType is provided" { + It "should throw an error" { + { Export-CacheObject -CacheType 'InvalidType' -Content @() } | Should -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Find-CacheItem.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Find-CacheItem.tests.ps1 new file mode 100644 index 000000000..9d37d4411 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Find-CacheItem.tests.ps1 @@ -0,0 +1,68 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Find-CacheItem.Tests.ps1 + +# Import the module or the script containing the function +# Import-Module 'Path\To\Your\Module.psd1' or . .\Path\To\YourScript.ps1 + +Describe 'Find-CacheItem' -Tags "Unit", "Cache" { + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Find-CacheItem.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + $testCacheList = @( + [PSCustomObject]@{ Name = 'Item1'; Value = @{ Name = 'Value1' } }, + [PSCustomObject]@{ Name = 'Item2'; Value = @{ Name = 'Value2' } }, + [PSCustomObject]@{ Name = 'MyCacheItem'; Value = @{ Name = 'MyValue' } } + ) + + } + + Context 'When a matching item exists' { + + It 'Returns the matching item' { + + $filter = { $_.Value.Name -eq 'MyValue' } + $result = $testCacheList | Find-CacheItem -Filter $filter + $result | Should -HaveCount 1 + $result.Name | Should -Be 'MyCacheItem' + } + + } + + Context 'When no matching item exists' { + It 'Returns an empty array' { + $filter = { $_.Name -eq 'NonExistentItem' } + $result = $testCacheList | Find-CacheItem -Filter $filter + $result | Should -BeNullOrEmpty + } + } + + Context 'With an empty cache list' { + It 'Returns an empty array' { + $emptyCacheList = @() + $filter = { $_.Name -eq 'AnyItem' } + $result = $emptyCacheList | Find-CacheItem -Filter $filter + $result | Should -BeNullOrEmpty + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheItem.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheItem.tests.ps1 new file mode 100644 index 000000000..1cf9d6eeb --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheItem.tests.ps1 @@ -0,0 +1,93 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-CacheItem' -Tags "Unit", "Cache" { + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-CacheItem.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('Type1', 'Type2') } + Mock -CommandName Get-CacheObject + + } + + Context 'Valid Cache Item' { + It 'Returns cache item value' { + $expectedValue = 'TestValue' + + Mock -CommandName Get-CacheObject -MockWith { + + $list = [System.Collections.Generic.List[CacheItem]]::New() + $listItem = [CacheItem]::New('MyKey', $expectedValue) + $list.Add($listItem) + + return $list + } + + $result = Get-CacheItem -Key 'MyKey' -Type 'Type1' + $result | Should -Be $expectedValue + } + } + + Context 'Cache item does not exist' { + It 'Returns $null when cache item is not found' { + Mock -CommandName Get-CacheObject -MockWith { + + $list = [System.Collections.Generic.List[CacheItem]]::New() + $listItem = [CacheItem]::New('MyKey', $expectedValue) + $list.Add($listItem) + + return $list + } + + $result = Get-CacheItem -Key 'NonExistentKey' -Type 'Type1' + $result | Should -Be $null + } + } + + Context 'Error handling' { + It 'Logs error to verbose stream and returns $null' { + Mock -CommandName Write-Verbose + Mock -CommandName Get-CacheObject -MockWith { throw 'Test exception' } + + $result = { Get-CacheItem -Key 'MyKey' -Type 'Type1' } | Should -Not -Throw + $result | Should -Be $null + + Assert-MockCalled -CommandName Write-Verbose -Exactly 1 + } + } + + Context 'Using Filter' { + It 'Applies provided filter to cache items' { + $filteredValue = 'FilteredValue' + + Mock -CommandName Get-CacheObject -MockWith { + $list = [System.Collections.Generic.List[CacheItem]]::New() + $list.Add([CacheItem]::New('OtherKey', 'OtherValue')) + $list.Add([CacheItem]::New('MyKey', $filteredValue)) + return $list + } + + $result = Get-CacheItem -Key 'MyKey' -Type 'Type1' -Filter { $_.Value -eq $filteredValue } + $result | Should -Be $filteredValue + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheObject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheObject.tests.ps1 new file mode 100644 index 000000000..51d4cd313 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Get-CacheObject.tests.ps1 @@ -0,0 +1,59 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-CacheObject Tests' -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-CacheObject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + $ENV:AZDODSC_CACHE_DIRECTORY = "C:\MockCacheDirectory" + + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('Project', 'Team', 'Group', 'SecurityDescriptor') } + Mock -CommandName Import-CacheObject + + } + + AfterAll { + + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + + if ($originalEnvironment) { + Set-Variable -Name "ENV" -Value $originalEnvironment -Scope Global + } + } + + It 'Should throw error if environment variable is not set' { + $ENV:AZDODSC_CACHE_DIRECTORY = $null + { Get-CacheObject -CacheType 'Project' } | Should -Throw "The environment variable 'AZDODSC_CACHE_DIRECTORY' is not set. Please set the variable to the path of the cache directory." + } + + It 'Should return cache object from memory if available' { + $env:AZDODSC_CACHE_DIRECTORY = "C:\MockCacheDirectory" + Set-Variable -Name "AzDoProject" -Value "ProjectCache" -Scope Global + $result = Get-CacheObject -CacheType 'Project' + $result | Should -Be "ProjectCache" + Remove-Variable -Name "AzDoProject" -Scope Global -ErrorAction SilentlyContinue + } + + It 'Should import cache object if not available in memory' { + $env:AZDODSC_CACHE_DIRECTORY = "C:\MockCacheDirectory" + Mock -CommandName Import-CacheObject -MockWith { return "ImportedProjectCache" } + + $result = Get-CacheObject -CacheType 'Project' + $result | Should -Be "ImportedProjectCache" + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Import-CacheObject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Import-CacheObject.tests.ps1 new file mode 100644 index 000000000..a3e7b8c0f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Import-CacheObject.tests.ps1 @@ -0,0 +1,82 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Import-CacheObject Tests" -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Import-CacheObject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + # Mock the necessary Commands and Variables + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('Project', 'Team', 'Group', 'SecurityDescriptor') } + Mock -CommandName Test-Path -MockWith { return $true } + Mock -CommandName Import-Clixml -MockWith { return @([PSCustomObject]@{ Key = 'Key1'; Value = 'Value1' }, [PSCustomObject]@{ Key = 'Key2'; Value = 'Value2' }) } + Mock -CommandName Set-Variable + $ENV:AZDODSC_CACHE_DIRECTORY = 'C:\Cache' + + } + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + Context "Valid CacheType parameter" { + + It "Imports the cache object successfully" { + Import-CacheObject -CacheType 'Project' + + Assert-MockCalled -CommandName Test-Path -Exactly 1 + Assert-MockCalled -CommandName Import-Clixml -Exactly 1 + Assert-MockCalled -CommandName Set-Variable -Exactly 1 + } + + It "Sets the correct variable name and value" { + Import-CacheObject -CacheType 'Team' + + Assert-MockCalled -CommandName Set-Variable -Exactly 1 -ParameterFilter { + $Name -eq 'AzDoTeam' -and $Scope -eq 'Global' -and $Force -eq $true + } + } + } + + Context "Cache file not found" { + BeforeEach { + Mock -CommandName Test-Path -MockWith { return $false } + Mock -CommandName Write-Warning + } + + It "Writes a warning when cache file is not found" { + Import-CacheObject -CacheType 'Project' + + Assert-MockCalled -CommandName Write-Warning -Exactly 1 -ParameterFilter { + $Message -match 'Cache file not found' + } + } + } + + Context "Environment variable not set" { + BeforeEach { + $ENV:AZDODSC_CACHE_DIRECTORY = $null + } + + It "Throws an exception if the environment variable is not set" { + { + Import-CacheObject -CacheType 'Project' + } | Should -Throw + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Initialize-CacheObject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Initialize-CacheObject.tests.ps1 new file mode 100644 index 000000000..04f471ec7 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Initialize-CacheObject.tests.ps1 @@ -0,0 +1,101 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Initialize-CacheObject Tests" -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Initialize-CacheObject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + # Mock the necessary Commands and Variables + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('LiveProject', 'LiveProjects', 'Project', 'Team', 'Group', 'SecurityDescriptor') } + Mock -CommandName Test-Path -MockWith { param($Path) return $false } + Mock -CommandName Import-CacheObject + Mock -CommandName Set-CacheObject + Mock -CommandName Remove-Item + Mock -CommandName New-Item + + $ENV:AZDODSC_CACHE_DIRECTORY = 'C:\Cache' + + } + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + Context "Valid CacheType parameter" { + + It "Imports the cache object if cache file exists" { + + Mock Test-Path { $true } + + Initialize-CacheObject -CacheType 'Project' + + Assert-MockCalled -CommandName Import-CacheObject -Exactly 1 + Assert-MockCalled -CommandName Set-CacheObject -Exactly 0 + } + + It "Creates a new cache object if cache file does not exist" { + Mock Test-Path { $false } + + Initialize-CacheObject -CacheType 'Project' + + Assert-MockCalled -CommandName Import-CacheObject -Exactly 0 + Assert-MockCalled -CommandName Set-CacheObject -Exactly 1 + } + + It "Removes cache file if BypassFileCheck is not present and CacheType matches '^Live'" { + Mock Test-Path -MockWith { $true } -ParameterFilter { $ErrorAction -eq 'SilentlyContinue' } + Mock Test-Path -MockWith { $false } -ParameterFilter { $Path -eq 'C:\Cache\Cache\LiveProject.clixml' } + + Initialize-CacheObject -CacheType 'LiveProject' + + Assert-MockCalled -CommandName Remove-Item -Times 1 + Assert-MockCalled -CommandName Import-CacheObject -Exactly 0 + Assert-MockCalled -CommandName Set-CacheObject -Times 1 + } + } + + Context "Environment variable not set" { + BeforeEach { + $ENV:AZDODSC_CACHE_DIRECTORY = $null + } + + It "Throws an exception if the environment variable is not set" { + { + Initialize-CacheObject -CacheType 'Project' + } | Should -Throw "*The environment variable `'AZDODSC_CACHE_DIRECTORY`' is not set.*" + } + } + + Context "BypassFileCheck switch" { + + BeforeAll { + $ENV:AZDODSC_CACHE_DIRECTORY = 'C:\Cache' + } + + It "Does not remove cache file if BypassFileCheck is present" { + Mock Test-Path { $true } -ParameterFilter { $Path -ne $null } + + Initialize-CacheObject -CacheType 'LiveProjects' -BypassFileCheck + + Assert-MockCalled -CommandName Remove-Item -Exactly 0 + Assert-MockCalled -CommandName Import-CacheObject -Exactly 1 + Assert-MockCalled -CommandName Set-CacheObject -Exactly 0 + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-AzDoCache.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-AzDoCache.tests.ps1 new file mode 100644 index 000000000..ebaf91b26 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-AzDoCache.tests.ps1 @@ -0,0 +1,61 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Refresh-AzDoCache Tests" -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Refresh-AzDoCache.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + # Mock the Get-Command cmdlet to return a list of commands matching the pattern + Mock -CommandName Get-Command -MockWith { + @( + [pscustomobject]@{ Name = 'AzDoAPI_CacheType1'; Source = 'AzureDevOpsDsc.Common' }, + [pscustomobject]@{ Name = 'AzDoAPI_CacheType2'; Source = 'AzureDevOpsDsc.Common' } + ) + } + + function AzDoAPI_CacheType1 { + param ($OrganizationName) + } + function AzDoAPI_CacheType2 { + param ($OrganizationName) + } + + # Mock the commands that would be invoked by Refresh-AzDoCache + Mock -CommandName AzDoAPI_CacheType1 + Mock -CommandName AzDoAPI_CacheType2 + Mock -CommandName Remove-Variable + Mock -CommandName Import-CacheObject + + } + + Context "When OrganizationName is provided" { + It "Should call each caching command with the correct OrganizationName parameter" { + $orgName = 'MyOrg' + Refresh-AzDoCache -OrganizationName $orgName + + # Verify that Get-Command was called with the correct parameters + Assert-MockCalled -CommandName Get-Command + + # Verify that each caching command was called with the correct OrganizationName parameter + Assert-MockCalled -CommandName AzDoAPI_CacheType1 + Assert-MockCalled -CommandName AzDoAPI_CacheType2 + + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheIdentity.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheIdentity.tests.ps1 new file mode 100644 index 000000000..359389cfe --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheIdentity.tests.ps1 @@ -0,0 +1,77 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Refresh-CacheIdentity' -Tags "Unit", "Cache" { + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Refresh-CacheIdentity.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('TypeA', 'TypeB', 'TypeC') } + Mock -CommandName Get-DevOpsDescriptorIdentity -MockWith { + return [PSCustomObject]@{ + id = 'id123' + descriptor = 'descriptor123' + subjectDescriptor = 'subjectDescriptor123' + providerDisplayName = 'providerDisplayName123' + isActive = $true + isContainer = $false + } + } + Mock -CommandName Add-CacheItem -MockWith {} + Mock -CommandName Get-CacheObject -MockWith { return @() } + Mock -CommandName Set-CacheObject -MockWith {} + + $global:DSCAZDO_OrganizationName = 'TestOrg' + $key = 'testKey' + $cacheType = 'TypeA' + + } + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -ErrorAction SilentlyContinue + } + + BeforeEach { + $identity = [PSCustomObject]@{ descriptor = 'descriptor123' } + } + + It 'Adds ACLIdentity to Identity' { + + Refresh-CacheIdentity -Identity $identity -Key $key -CacheType $cacheType + + $identity.PSObject.Properties.Match('ACLIdentity').Count | Should -Be 1 + $identity.ACLIdentity.id | Should -Be 'id123' + } + + It 'Should not throw an error' { + $ErrorActionPreference = 'Stop' + { Refresh-CacheIdentity -Identity $identity -Key $key -CacheType $cacheType } | Should -Not -Throw + } + + It 'Should not throw an error when there are duplicate cache objects' { + $ErrorActionPreference = 'Stop' + { Refresh-CacheIdentity -Identity $identity -Key $key -CacheType $cacheType } | Should -Not -Throw + { Refresh-CacheIdentity -Identity $identity -Key $key -CacheType $cacheType } | Should -Not -Throw + } + + + It 'Calls Add-CacheItem with correct parameters' { + Refresh-CacheIdentity -Identity $identity -Key $key -CacheType $cacheType + + Assert-MockCalled -CommandName Add-CacheItem -Exactly 1 + } + + It 'Calls Set-CacheObject with current cache' { + Refresh-CacheIdentity -Identity $identity -Key $key -CacheType $cacheType + + Assert-MockCalled -CommandName Set-CacheObject -Exactly 1 -Scope It + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheObject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheObject.tests.ps1 new file mode 100644 index 000000000..aab3d497e --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Refresh-CacheObject.tests.ps1 @@ -0,0 +1,65 @@ +# Tests for Refresh-CacheObject function + +# Import the module containing the Refresh-CacheObject function if necessary +# Import-Module YourModuleName + +$currentFile = $MyInvocation.MyCommand.Path + + +Describe "Refresh-CacheObject" -tags Unit, Cache { + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Refresh-CacheObject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + # Mock the Get-AzDoCacheObjects function to return a list of valid cache types + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('Type1', 'Type2', 'Type3') } + + # Mock the Remove-Variable cmdlet to prevent actual removal of variables + Mock -CommandName Remove-Variable + + # Mock the Import-CacheObject function to prevent actual import actions + Mock -CommandName Import-CacheObject + + } + + Context "When CacheType is valid" { + It "Should unload and reload the cache object of type 'Type1'" { + $cacheType = 'Type1' + Refresh-CacheObject -CacheType $cacheType + + # Verify that Remove-Variable was called with the correct parameters + Assert-MockCalled -CommandName Remove-Variable -Exactly 1 -ParameterFilter { $Name -eq "AzDoType1" } + + # Verify that Import-CacheObject was called with the correct parameter + Assert-MockCalled -CommandName Import-CacheObject -Exactly 1 -ParameterFilter { $CacheType -eq 'Type1' } + } + + It "Should unload and reload the cache object of type 'Type2'" { + $cacheType = 'Type2' + Refresh-CacheObject -CacheType $cacheType + + # Verify that Remove-Variable was called with the correct parameters + Assert-MockCalled -CommandName Remove-Variable -Exactly 1 -ParameterFilter { $Name -eq "AzDoType2" } + + # Verify that Import-CacheObject was called with the correct parameter + Assert-MockCalled -CommandName Import-CacheObject -Exactly 1 -ParameterFilter { $CacheType -eq 'Type2' } + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Remove-CacheItem.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Remove-CacheItem.tests.ps1 new file mode 100644 index 000000000..4de5d30af --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Remove-CacheItem.tests.ps1 @@ -0,0 +1,75 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-CacheItem' -Tags "Unit", "Cache" { + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + BeforeAll { + + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-CacheItem.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + Mock -CommandName Get-CacheObject -MockWith { + param ([string]$CacheType) + + $list = [System.Collections.Generic.List[CacheItem]]::New() + + switch ($CacheType) + { + "Project" { + $list.Add([CacheItem]::New("myKey", "someValue")) + } + "Group" { + $list.Add([CacheItem]::New("anotherKey", "anotherValue")) + } + default { + throw "Invalid CacheType" + } + } + + return $list + + } + + Mock -CommandName Set-Variable -MockWith {} + + } + + It 'Removes item from Project cache when key matches' { + $cache = Get-CacheObject -CacheType "Project" + Remove-CacheItem -Key "myKey" -Type "Project" + $global:AzDoProject | Should -BeNullOrEmpty + } + + It 'Removes item from Group cache when key matches' { + $cache = Get-CacheObject -CacheType "Group" + Remove-CacheItem -Key "anotherKey" -Type "Group" + $global:AzDoGroup | Should -BeNullOrEmpty + } + + It 'Handles non-matching key correctly' { + $cache = Get-CacheObject -CacheType "Group" + Remove-CacheItem -Key "nonMatchingKey" -Type "Group" + $global:AzDoGroup | Should -Be $null + } + + It 'Validates Type parameter against cache objects' { + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('Project', 'Group', 'Team', 'SecurityDescriptor') } + { Remove-CacheItem -Key "sampleKey" -Type "InvalidType" } | Should -Throw + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Set-CacheObject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Set-CacheObject.tests.ps1 new file mode 100644 index 000000000..269505a87 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Cache/Set-CacheObject.tests.ps1 @@ -0,0 +1,65 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-CacheObject' -Tags "Unit", "Cache" { + + BeforeAll { + + # Set the Project + $null = Set-Variable -Name "AzDoProject" -Value @() -Scope Global + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-CacheObject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @('Project', 'Team', 'Group', 'SecurityDescriptor') } + Mock -CommandName Export-CacheObject -MockWith {} + + } + + AfterAll { + Remove-Variable -Name AzDoProject -ErrorAction SilentlyContinue + } + + Context 'When setting Project cache' { + + It 'should set the global variable AzDoProject' { + $content = @('Project1', 'Project2') + $global:AzDoProject = $null + + Set-CacheObject -CacheType 'Project' -Content $content -Depth 2 + + $global:AzDoProject | Should -Be $content + } + + It 'should call Export-CacheObject with correct parameters' { + $content = @('Project1', 'Project2') + + Set-CacheObject -CacheType 'Project' -Content $content -Depth 2 + + Assert-MockCalled Export-CacheObject -Exactly -Times 1 -ParameterFilter { + $CacheType -eq 'Project' -and + $Depth -eq 2 + } + } + + It 'should throw an error if CacheType is invalid' { + { Set-CacheObject -CacheType 'InvalidType' -Content @('data') } | Should -Throw + } + + It 'should throw an error if Export-CacheObject fails' { + Mock -CommandName Export-CacheObject -MockWith { throw "Export failed" } + Mock -CommandName Write-Error -Verifiable + + { Set-CacheObject -CacheType 'Project' -Content @('data') } | Should -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiHttpRequestHeader.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiHttpRequestHeader.Tests.ps1 deleted file mode 100644 index b03170072..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiHttpRequestHeader.Tests.ps1 +++ /dev/null @@ -1,87 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Valid' - $testCasesInvalidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - It 'Should not throw - ""' -TestCases $testCasesValidPats { - param ([System.String]$Pat) - - { Get-AzDevOpsApiHttpRequestHeader -Pat $Pat } | Should -Not -Throw - } - - It 'Should output a "Hashtable" type - ""' -TestCases $testCasesValidPats { - param ([System.String]$Pat) - - $httpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - - $httpRequestHeader.GetType() | Should -Be $(@{}.GetType()) - } - - It 'Should output a "Hashtable" type containing an "Authorization" key - ""' -TestCases $testCasesValidPats { - param ([System.String]$Pat) - - $httpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - - $httpRequestHeader.ContainsKey('Authorization') | Should -BeTrue - } - - It 'Should output a "Hashtable" type containing an "Authorization" key that has a value beginning with "Basic " - ""' -TestCases $testCasesValidPats { - param ([System.String]$Pat) - - $httpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - - $httpRequestHeader.Authorization | Should -BeLike "Basic *" - } - - It 'Should output a "Hashtable" type containing an "Authorization" key that has a value as expected - "" ' -TestCases $testCasesValidPats { - param ([System.String]$Pat) - - $authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$Pat")) - $httpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - - $httpRequestHeader.Authorization | Should -BeExactly $authorization - } - - It 'Should output a "Hashtable" type that is successfully validated by "Test-AzDevOpsApiHttpRequestHeader" - ""' -TestCases $testCasesValidPats { - param ([System.String]$Pat) - - $httpRequestHeader = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat - - Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $httpRequestHeader -IsValid | Should -BeTrue - } - - } - - - Context "When input parameters are invalid" { - - It 'Should throw - ""' -TestCases $testCasesInvalidPats { - param ([System.String]$Pat) - - { Get-AzDevOpsApiHttpRequestHeader -Pat $Pat } | Should -Throw - } - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResource.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResource.Tests.ps1 deleted file mode 100644 index 922fb886c..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResource.Tests.ps1 +++ /dev/null @@ -1,278 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - # Get default, parameter values - $defaultApiVersion = Get-AzDevOpsApiVersion -Default - - # Helper function for generating fake resource, JSON response - Function Get-MockResourceJson - { - return '{ - "count": 3, - "value": [ - { - "id": "8d4bff8d-6169-45cf-b085-fe12ad67e76b", - "name": "Test Resource 1", - "description": "Test Resource Description 1", - "url": "https://dev.azure.com/fabrikam/_apis/resources/8d4bff8d-6169-45cf-b085-fe12ad67e76b", - "state": "wellFormed" - }, - { - "id": "114bff8d-6169-45cf-b085-fe121267e7aa", - "name": "Test Resource 2", - "description": "Test Resource Description 2", - "url": "https://dev.azure.com/fabrikam/_apis/resources/114bff8d-6169-45cf-b085-fe121267e7aa", - "state": "wellFormed" - }, - { - "id": "a654b805-6be9-477b-a00c-bd76949192c3", - "name": "Test Resource 3", - "description": "Test Resource Description 3", - "url": "https://dev.azure.com/fabrikam/_apis/resources/a654b805-6be9-477b-a00c-bd76949192c3", - "state": "wellFormed" - } - ] - }' - } - $noOfMockResources = $((Get-MockResourceJson | ConvertFrom-Json).value).Count - $resourceIdThatExists = '8d4bff8d-6169-45cf-b085-fe12ad67e76b' # Same as 'Test Resource 1' in 'Get-MockResourceJson', JSON output - $resourceIdThatDoesNotExist = '7f5a49c8-9424-4ec5-b4b7-1dc76cd05149' - $resourceIdThatIsInvalid = Get-TestCaseValue -ScopeName 'ResourceId' -TestCaseName 'Invalid' - - # Mock functions called in function - Mock Invoke-AzDevOpsApiRestMethod { - - #$resourceIdThatExists = '8d4bff8d-6169-45cf-b085-fe12ad67e76b' - [PSObject]$resources = Get-MockResourceJson | ConvertFrom-Json - - if (![string]::IsNullOrWhiteSpace($ResourceId)) - { - [PSObject[]]$resources = $resources.value - [PSObject]$resources = $resources | - Where-Object { $_.id -eq $ResourceId} - } - - return $resources - } - # Mock Get-AzDevOpsApiResourceUri # Do not mock - # Mock Get-AzDevOpsApiHttpRequestHeader # Do not mock - - # Generate valid, test cases - $testCasesValidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Valid' - $testCasesValidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Valid' - $testCasesValidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Valid' - $testCasesValidApiUriPatResourceNames = Join-TestCaseArray -TestCaseArray @( - $testCasesValidApiUris, - $testCasesValidPats, - $testCasesValidResourceNames) -Expand - $testCasesValidApiUriPatResourceNames3 = $testCasesValidApiUriPatResourceNames | Select-Object -First 3 - - $validApiVersion = Get-TestCaseValue -ScopeName 'ApiVersion' -TestCaseName 'Valid' -First 1 - - # Generate invalid, test cases - $testCasesInvalidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Invalid' - $testCasesInvalidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Invalid' - $testCasesInvalidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Invalid' - $testCasesInvalidApiUriPatResourceNames = Join-TestCaseArray -TestCaseArray @( - $testCasesInvalidApiUris, - $testCasesInvalidPats, - $testCasesInvalidResourceNames) -Expand - $testCasesInvalidApiUriPatResourceNames3 = $testCasesInvalidApiUriPatResourceNames | Select-Object -First 3 - - $invalidApiVersion = Get-TestCaseValue -ScopeName 'ApiVersion' -TestCaseName 'Invalid' -First 1 - - - Context 'When input parameters are valid' { - - Context 'When called with mandatory, "ApiUri", "Pat" and "ResourceName" parameters' { - - It 'Should not throw - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName } | Should -Not -Throw - } - - It 'Should return a type of "System.Management.Automation.PsObject[]" - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - [System.Management.Automation.PsObject[]]$resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName - - $true | Should -Be $true # Note: Will always evaluate true (but strong-typing of $resources variable would fail this test anyway) - } - - It 'Should return all resources - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - $resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName - - $resources.Count | Should -Be $noOfMockResources - } - - It 'Should invoke "Get-AzDevOpsApiResourceUri" only once - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - Mock Get-AzDevOpsApiResourceUri { - return "http://someUri.api/" - } -Verifiable - - $resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName - - Assert-MockCalled 'Get-AzDevOpsApiResourceUri' -Times 1 -Exactly -Scope 'It' - } - - It 'Should invoke "Get-AzDevOpsApiHttpRequestHeader" only once - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - Mock Get-AzDevOpsApiHttpRequestHeader { - Get-TestCaseValue -ScopeName 'HttpRequestHeader' -TestCaseName 'Valid' -First 1 - } -Verifiable - - $resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName - - Assert-MockCalled 'Get-AzDevOpsApiHttpRequestHeader' -Times 1 -Exactly -Scope 'It' - } - - It 'Should invoke "Invoke-AzDevOpsApiRestMethod" only once - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - Mock Invoke-AzDevOpsApiRestMethod {} -Verifiable - - $resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName - - Assert-MockCalled 'Invoke-AzDevOpsApiRestMethod' -Times 1 -Exactly -Scope 'It' - } - - } - - - Context 'When called with mandatory, "ApiUri", "Pat", "ResourceName" and "ResourceId" parameters' { - - Context 'When the "ResourceId" parameter value is invalid' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatIsInvalid } | Should -Throw - } - } - - Context 'When a resource with the "ResourceId" parameter value does exist' { - - It 'Should not throw - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatExists } | Should -Not -Throw - } - - It 'Should return a type of "System.Management.Automation.PsObject[]" - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - [System.Management.Automation.PsObject[]]$resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatExists - - $true | Should -Be $true # Note: Will always evaluate true (but strong-typing of $resources variable would fail this test anyway) - } - - It 'Should not return a $null - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - [System.Management.Automation.PsObject]$resource = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatExists - - $resource | Should -Not -BeNullOrEmpty - } - - It 'Should return a resource with the correct "id"/"ResourceId" - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - [System.Management.Automation.PsObject]$resource = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatExists - - $resource.id | Should -Be $resourceIdThatExists - } - } - - - Context 'When a resource with the "ResourceId" parameter value does not exist' { - - It 'Should not throw - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatDoesNotExist } | Should -Not -Throw - } - - It 'Should return a type of "System.Management.Automation.PsObject[]" - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - [System.Management.Automation.PsObject[]]$resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatDoesNotExist - - $true | Should -Be $true # Note: Will always evaluate true (but strong-typing of $resources variable would fail this test anyway) - } - - It 'Should return no resources - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - [System.Management.Automation.PsObject[]]$resources = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatDoesNotExist - - $resources.Count | Should -Be 0 - } - - It 'Should return $null - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - $resource = Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $resourceIdThatDoesNotExist - - $resource | Should -Be $null - } - } - - - Context "When also called with valid 'ApiVersion' parameter value" { - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Get-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $validApiVersion -Pat $Pat -ResourceName $ResourceName } | Should -Not -Throw - } - } - - Context "When also called with invalid 'ApiVersion' parameter value" { - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Get-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $invalidApiVersion -Pat $Pat -ResourceName $ResourceName } | Should -Throw - } - } - } - } - - - Context 'When input parameters are invalid' { - - Context 'When called with mandatory, "ApiUri", "Pat" and "ResourceName" parameters' { - - It 'Should throw - "", "", ""' -TestCases $testCasesinvalidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Get-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName } | Should -Throw - } - } - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceName.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceName.Tests.ps1 deleted file mode 100644 index 68f829c67..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiResourceName.Tests.ps1 +++ /dev/null @@ -1,80 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - - Context 'When input parameters are valid' { - - It 'Should not throw' { - - { Get-AzDevOpsApiResourceName } | Should -Not -Throw - } - - It 'Should output a "System.String[]" type containing atleast 1 string value' { - - [System.String[]]$resourceNames = Get-AzDevOpsApiResourceName - - $resourceNames.Count | Should -BeGreaterThan 0 - } - - It 'Should output a "System.String[]" type containing no empty values' { - - [System.String[]]$resourceNames = Get-AzDevOpsApiResourceName - - [System.String]::Empty | Should -Not -BeIn $resourceNames - } - - It 'Should output a "System.String[]" type containing no $null values' { - - [System.String[]]$resourceNames = Get-AzDevOpsApiResourceName - - $null | Should -Not -BeIn $resourceNames - } - - It 'Should output a "System.String[]" type containing unique values' { - - [System.String[]]$resourceNames = Get-AzDevOpsApiResourceName - - $resourceNames.Count | Should -Be $($resourceNames | Select-Object -Unique).Count - } - - # Create test cases for each 'ResourceName' returned by 'Get-AzDevOpsApiResourceName' - [Hashtable[]]$testCasesResourceNames = Get-AzDevOpsApiResourceName | - ForEach-Object { - @{ - ResourceName = $_ - } - } - - It 'Should output values that are all validated by "Test-AzDevOpsApiResourceName" - ""' -TestCases $testCasesResourceNames { - param ([System.String]$ResourceName) - - Test-AzDevOpsApiResourceName -ResourceName $ResourceName -IsValid | Should -BeTrue - } - - } - - - Context "When input parameters are invalid" { - - # N/A - No parameters passed to function - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriAreaName.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriAreaName.Tests.ps1 deleted file mode 100644 index d075813bd..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriAreaName.Tests.ps1 +++ /dev/null @@ -1,127 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Valid' - $testCasesInvalidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with no parameter values' { - - It 'Should not throw' { - - { Get-AzDevOpsApiUriAreaName } | Should -Not -Throw - } - - It 'Should output a "System.String[]" type containing more than 1 value' { - - [System.String[]]$uriAreaNames = Get-AzDevOpsApiUriAreaName - - $uriAreaNames.Count | Should -BeGreaterThan 1 - } - - It 'Should output a "System.String[]" type containing no empty values' { - - [System.String[]]$uriAreaNames = Get-AzDevOpsApiUriAreaName - - [System.String]::Empty | Should -Not -BeIn $uriAreaNames - } - - It 'Should output a "System.String[]" type containing no $null values' { - - [System.String[]]$uriAreaNames = Get-AzDevOpsApiUriAreaName - - $null | Should -Not -BeIn $uriAreaNames - } - - It 'Should output a "System.String[]" type containing unique values' { - - [System.String[]]$uriAreaNames = Get-AzDevOpsApiUriAreaName - - $uriAreaNames.Count | Should -Be $($uriAreaNames | Select-Object -Unique).Count - } - - # Create test cases for each 'UriResourceName' returned by 'Get-AzDevOpsApiUriAreaName' - #[Hashtable[]]$testCasesUriResourceNames = Get-AzDevOpsApiUriAreaName | - # ForEach-Object { - # @{ - # UriResourceName = $_ - # } - # } - - # TODO: Uncomment this test once 'Test-AzDevOpsApiUriAreaName' function available - #It 'Should output values that are all validated by "Test-AzDevOpsApiUriAreaName" - ""' -TestCases $testCasesUriResourceNames { - # param ([System.String]$UriResourceName) - # - # Test-AzDevOpsApiUriAreaName -UriResourceName $UriResourceName -IsValid | Should -BeTrue - #} - } - - - Context 'When called with a "ResourceName" parameter value' { - - It 'Should not throw - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - { Get-AzDevOpsApiUriAreaName -ResourceName $ResourceName } | Should -Not -Throw - } - - It 'Should output a "System.String[]" type containing exactly 1 value - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - [System.String[]]$uriAreaNames = Get-AzDevOpsApiUriAreaName -ResourceName $ResourceName - - $uriAreaNames.Count | Should -BeExactly 1 - } - - It 'Should output a "System.String" type that is not null or empty - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - [System.String]$uriResourceName = Get-AzDevOpsApiUriAreaName -ResourceName $ResourceName - - $uriResourceName | Should -Not -BeNullOrEmpty - } - - It 'Should output a "System.String" type that is lowercase - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - [System.String]$uriResourceName = Get-AzDevOpsApiUriAreaName -ResourceName $ResourceName - - $uriResourceName | Should -BeExactly $($uriResourceName.ToLower()) - } - } - } - - - Context "When input parameters are invalid" { - - Context 'When called with a "ResourceName" parameter value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidResourceNames { - param ([System.String]$ResourceName) - - { Get-AzDevOpsApiUriAreaName -ResourceName $ResourceName } | Should -Throw - } - } - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriResourceName.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriResourceName.Tests.ps1 deleted file mode 100644 index 639639ea3..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiUriResourceName.Tests.ps1 +++ /dev/null @@ -1,127 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Valid' - $testCasesInvalidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with no parameter values' { - - It 'Should not throw' { - - { Get-AzDevOpsApiUriResourceName } | Should -Not -Throw - } - - It 'Should output a "System.String[]" type containing more than 1 value' { - - [System.String[]]$uriResourceNames = Get-AzDevOpsApiUriResourceName - - $uriResourceNames.Count | Should -BeGreaterThan 1 - } - - It 'Should output a "System.String[]" type containing no empty values' { - - [System.String[]]$uriResourceNames = Get-AzDevOpsApiUriResourceName - - [System.String]::Empty | Should -Not -BeIn $uriResourceNames - } - - It 'Should output a "System.String[]" type containing no $null values' { - - [System.String[]]$uriResourceNames = Get-AzDevOpsApiUriResourceName - - $null | Should -Not -BeIn $uriResourceNames - } - - It 'Should output a "System.String[]" type containing unique values' { - - [System.String[]]$uriResourceNames = Get-AzDevOpsApiUriResourceName - - $uriResourceNames.Count | Should -Be $($uriResourceNames | Select-Object -Unique).Count - } - - # Create test cases for each 'UriResourceName' returned by 'Get-AzDevOpsApiUriResourceName' - #[Hashtable[]]$testCasesUriResourceNames = Get-AzDevOpsApiUriResourceName | - # ForEach-Object { - # @{ - # UriResourceName = $_ - # } - # } - - # TODO: Uncomment this test once 'Test-AzDevOpsApiUriResourceName' function available - #It 'Should output values that are all validated by "Test-AzDevOpsApiUriResourceName" - ""' -TestCases $testCasesUriResourceNames { - # param ([System.String]$UriResourceName) - # - # Test-AzDevOpsApiUriResourceName -UriResourceName $UriResourceName -IsValid | Should -BeTrue - #} - } - - - Context 'When called with a "ResourceName" parameter value' { - - It 'Should not throw - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - { Get-AzDevOpsApiUriResourceName -ResourceName $ResourceName } | Should -Not -Throw - } - - It 'Should output a "System.String[]" type containing exactly 1 value - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - [System.String[]]$uriResourceNames = Get-AzDevOpsApiUriResourceName -ResourceName $ResourceName - - $uriResourceNames.Count | Should -BeExactly 1 - } - - It 'Should output a "System.String" type that is not null or empty - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - [System.String]$uriResourceName = Get-AzDevOpsApiUriResourceName -ResourceName $ResourceName - - $uriResourceName | Should -Not -BeNullOrEmpty - } - - It 'Should output a "System.String" type that is lowercase - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - [System.String]$uriResourceName = Get-AzDevOpsApiUriResourceName -ResourceName $ResourceName - - $uriResourceName | Should -BeExactly $($uriResourceName.ToLower()) - } - } - } - - - Context "When input parameters are invalid" { - - Context 'When called with a "ResourceName" parameter value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidResourceNames { - param ([System.String]$ResourceName) - - { Get-AzDevOpsApiUriResourceName -ResourceName $ResourceName } | Should -Throw - } - } - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiVersion.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiVersion.Tests.ps1 deleted file mode 100644 index 8e28c30b7..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiVersion.Tests.ps1 +++ /dev/null @@ -1,141 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidApiVersions = Get-TestCase -ScopeName 'ApiVersion' -TestCaseName 'Valid' - $testCasesInvalidApiVersions = Get-TestCase -ScopeName 'ApiVersion' -TestCaseName 'Invalid' - $supportedApiVersion = '6.0' - - - Context 'When input parameters are valid' { - - - Context 'When called with no parameter values' { - - It 'Should not throw' { - - { Get-AzDevOpsApiVersion } | Should -Not -Throw - } - - # Note: Only applicable if only 1 'ApiVersion' is supported - It "Should output a 'System.String' type containing an 'ApiVersion' of '$supportedApiVersion'" { - - [System.String]$apiVersion = Get-AzDevOpsApiVersion - - $apiVersion | Should -BeExactly $supportedApiVersion - } - - # Note: Only applicable if only 1 'ApiVersion' is supported - It 'Should output a "System.String[]" type containing no empty values' { - - [System.String[]]$apiVersions = Get-AzDevOpsApiVersion - - [System.String]::Empty | Should -Not -BeIn $apiVersions - } - - It 'Should output a "System.String[]" type containing no $null values' { - - [System.String[]]$apiVersions = Get-AzDevOpsApiVersion - - $null | Should -Not -BeIn $apiVersions - } - - It 'Should output a "System.String[]" type containing unique values' { - - [System.String[]]$apiVersions = Get-AzDevOpsApiVersion - - $apiVersions.Count | Should -Be $($apiVersions | Select-Object -Unique).Count - } - - #Create test cases for each 'ApiVersion' returned by 'Get-AzDevOpsApiVersion' - [Hashtable[]]$testCasesApiVersions = Get-AzDevOpsApiVersion | - ForEach-Object { - @{ - ApiVersion = $_ - } - } - - It 'Should output values that are all validated by "Test-AzDevOpsApiVersion" - ""' -TestCases $testCasesApiVersions { - param ([System.String]$ApiVersion) - - Test-AzDevOpsApiVersion -ApiVersion $ApiVersion -IsValid | Should -BeTrue - } - - It 'Should output values that are in the valid, "ApiVersion" test cases - ""' -TestCases $testCasesValidApiVersions { - param ([System.String]$ApiVersion) - - $ApiVersion | Should -BeIn $([System.String[]]$(Get-AzDevOpsApiVersion)) - } - - It 'Should not output values that are in the invalid, "ApiVersion" test cases - ""' -TestCases $testCasesInvalidApiVersions { - param ([System.String]$ApiVersion) - - $ApiVersion | Should -Not -BeIn $([System.String[]]$(Get-AzDevOpsApiVersion)) - } - } - - - Context 'When called with the "Default" switch parameter' { - - It 'Should not throw' { - - { Get-AzDevOpsApiVersion -Default } | Should -Not -Throw - } - - It 'Should output a "System.String[]" type containing exactly 1 value' { - - [System.String[]]$apiVersions = Get-AzDevOpsApiVersion -Default - - $apiVersions.Count | Should -BeExactly 1 - } - - It 'Should output a "System.String" type that is not null or empty' { - - [System.String]$uriResourceName = Get-AzDevOpsApiVersion -Default - - $uriResourceName | Should -Not -BeNullOrEmpty - } - - It "Should output a 'System.String' type containing an 'ApiVersion' of '$supportedApiVersion'" { - - [System.String]$apiVersion = Get-AzDevOpsApiVersion -Default - - $apiVersion | Should -BeExactly $supportedApiVersion - } - } - - - # Effectively identical to 'When called with no parameter values' context (with test cases above) - Context 'When called with a "Default" switch parameter value of $false' { - - It 'Should not throw' { - - { Get-AzDevOpsApiVersion -Default:$false } | Should -Not -Throw - } - } - } - - - Context "When input parameters are invalid" { - - # N/A - Only the 'Default' switch parameter on this function/commands - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitIntervalMs.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitIntervalMs.Tests.ps1 deleted file mode 100644 index f79fa9e34..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitIntervalMs.Tests.ps1 +++ /dev/null @@ -1,51 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - [Int32]$expectedWaitIntervalMs = 500 - - - Context 'When input parameters are valid' { - - - Context 'When called with no parameter values' { - - It 'Should not throw' { - - { Get-AzDevOpsApiWaitIntervalMs } | Should -Not -Throw - } - - It "Should output a 'Int32' type containing an 'WaitIntervalMs' of '$expectedWaitIntervalMs'" { - - [Int32]$waitIntervalMs = Get-AzDevOpsApiWaitIntervalMs - - $waitIntervalMs | Should -BeExactly $expectedWaitIntervalMs - } - } - - } - - - Context "When input parameters are invalid" { - - # N/A - No parameters on this function/command - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitTimeoutMs.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitTimeoutMs.Tests.ps1 deleted file mode 100644 index 6f92ad0ab..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzDevOpsApiWaitTimeoutMs.Tests.ps1 +++ /dev/null @@ -1,51 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - [Int32]$expectedWaitTimeoutMs = 300000 - - - Context 'When input parameters are valid' { - - - Context 'When called with no parameter values' { - - It 'Should not throw' { - - { Get-AzDevOpsApiWaitTimeoutMs } | Should -Not -Throw - } - - It "Should output a 'Int32' type containing an 'WaitTimeoutMs' of '$expectedWaitTimeoutMs'" { - - [Int32]$waitTimeoutMs = Get-AzDevOpsApiWaitTimeoutMs - - $waitTimeoutMs | Should -BeExactly $expectedWaitTimeoutMs - } - } - - } - - - Context "When input parameters are invalid" { - - # N/A - No parameters on this function/command - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACEList.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACEList.tests.ps1 new file mode 100644 index 000000000..976453ec4 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACEList.tests.ps1 @@ -0,0 +1,78 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "ConvertTo-ACEList" -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'ConvertTo-ACEList.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Mock external functions + Mock -CommandName Find-Identity -MockWith { + return @{ + Identity = "MockIdentity" + } + } + + Mock -CommandName ConvertTo-ACETokenList -MockWith { + return @("MockPermission") + } + } + + It "should return a singular item of ACEs when valid parameters are provided" { + $result = ConvertTo-ACEList -SecurityNamespace "Namespace" -Permissions @( + @{ Identity = "User1"; Permission = "Read" } + ) -OrganizationName "MyOrg" + + $result | Should -Not -BeNullOrEmpty + $result.Identity.Identity | Should -Be "MockIdentity" + $result.Permissions | Should -Contain "MockPermission" + } + + It "should return multiple items of ACEs when valid parameters are provided" { + $result = ConvertTo-ACEList -SecurityNamespace "Namespace" -Permissions @( + @{ Identity = "User1"; Permission = "Read" }, + @{ Identity = "User2"; Permission = "Write" } + ) -OrganizationName "MyOrg" + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 2 + $result[0].Identity.Identity | Should -Be "MockIdentity" + $result[0].Permissions | Should -Contain "MockPermission" + $result[1].Identity.Identity | Should -Be "MockIdentity" + $result[1].Permissions | Should -Contain "MockPermission" + } + + It "should log a warning if the identity is not found" { + Mock -CommandName Find-Identity -MockWith { return $null } + Mock -CommandName Write-Warning -Verifiable + + { ConvertTo-ACEList -SecurityNamespace "Namespace" -Permissions @(@{ Identity = "User1"; Permission = "Read" }) -OrganizationName "MyOrg" } | Should -Not -Throw + Assert-VerifiableMock + + } + + It "should log a warning if permissions are not found" { + Mock -CommandName ConvertTo-ACETokenList -MockWith { return $null } + Mock -CommandName Write-Warning -Verifiable + + $result = ConvertTo-ACEList -SecurityNamespace "Namespace" -Permissions @(@{ Identity = "User1"; Permission = "Read" }) -OrganizationName "MyOrg" + $result | Should -BeNullOrEmpty + Assert-VerifiableMock + } + + It "should handle empty permissions array gracefully" { + $result = ConvertTo-ACEList -SecurityNamespace "Namespace" -Permissions @() -OrganizationName "MyOrg" + + $result | Should -BeNullOrEmpty + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACETokenList.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACETokenList.tests.ps1 new file mode 100644 index 000000000..d9b920142 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACETokenList.tests.ps1 @@ -0,0 +1,84 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "ConvertTo-ACETokenList Tests" -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'ConvertTo-ACETokenList.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Cache Item + . (Get-ClassFilePath '000.CacheItem') + + Mock -CommandName Get-AzDoCacheObjects -MockWith { + return @('SecurityNamespaces') + } + + } + + BeforeEach { + # Mock Get-CacheItem to return a mock SecurityDescriptor + Mock -CommandName Get-CacheItem -MockWith { + return @{ + actions = @( + @{ displayName = "Read"; name = "read" }, + @{ displayName = "Write"; name = "write" }, + @{ displayName = "Execute"; name = "execute" } + ) + } + } + } + + It "should return an empty list when SecurityDescriptor is not found" { + # Mock to return $null for not found SecurityDescriptor + Mock -CommandName Get-CacheItem -MockWith { return $null } + Mock -CommandName Write-Error -Verifiable + + $result = ConvertTo-ACETokenList -SecurityNamespace "TestNamespace" -ACEPermissions @( + @{ "Read" = "Allow"; "Write" = "Deny" } + ) + + $result | Should -BeNullOrEmpty + Assert-VerifiableMock + } + + It "should correctly process Allow and Deny permissions" { + $acePermissions = @( + @{ "Read" = "Allow"; "Write" = "Deny" }, + @{ "Execute" = "Allow"; "Read" = "Deny" } + ) + $result = ConvertTo-ACETokenList -SecurityNamespace "TestNamespace" -ACEPermissions $acePermissions + + $result | Should -HaveCount 2 + $result[0].DescriptorType | Should -Be "TestNamespace" + $result[0].Allow.displayName | Should -Contain "Read" + $result[0].Deny.displayName | Should -Contain "Write" + $result[1].Allow.displayName | Should -Contain "Execute" + $result[1].Deny.displayName | Should -Contain "Read" + } + + It "should filter out permissions not found in the SecurityDescriptor" { + $acePermissions = @( + @{ "UnknownPermission" = "Allow"; "Read" = "Deny"; "Write" = "Deny" } + ) + + $result = ConvertTo-ACETokenList -SecurityNamespace "TestNamespace" -ACEPermissions $acePermissions + + $result | Should -HaveCount 1 + $result.Allow | Should -BeNullOrEmpty + $result.Deny.displayName | Should -Contain "Read" + $result.Deny.displayName | Should -Contain "Write" + } + + +} + +# End of Pester tests for ConvertTo-ACETokenList diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACL.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACL.tests.ps1 new file mode 100644 index 000000000..3c6e7a19c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACL.tests.ps1 @@ -0,0 +1,57 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "ConvertTo-ACL" -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'ConvertTo-ACL.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + Mock -CommandName Write-Host + Mock -CommandName Write-Warning + Mock -CommandName New-ACLToken -MockWith { return @{ Token = "mockToken" } } + Mock -CommandName ConvertTo-ACEList -MockWith { + return @( + @{ Identity = "User1"; Permissions = "Read" }, + @{ Identity = "User2"; Permissions = "Read, Write" } + ) + } + Mock -CommandName Group-ACEs -MockWith { param($ACEs) return $ACEs } + + $permissions = @( + @{ + Identity = 'User1' + Permissions = 'Read' + }, + @{ + Identity = 'User2' + Permissions = 'Read', 'Write' + } + ) + + } + + It "should return an ACL with correct properties" { + $result = ConvertTo-ACL -Permissions $permissions -SecurityNamespace 'Namespace1' -isInherited $true -OrganizationName 'Org1' -TokenName 'Token1' + + $result | Should -Not -BeNullOrEmpty + $result.token.Token | Should -Be "mockToken" + $result.aces | Should -HaveCount 2 + $result.inherited | Should -Be $true + } + + It "should return a warning if no ACEs are created" { + Mock -CommandName ConvertTo-ACEList -MockWith { return @() } + $result = ConvertTo-ACL -Permissions $permissions -SecurityNamespace 'Namespace1' -isInherited $true -OrganizationName 'Org1' -TokenName 'Token1' + $result.aces | Should -HaveCount 0 + } +} + +# End of Pester tests for ConvertTo-ACL diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACLHashtable.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACLHashtable.tests.ps1 new file mode 100644 index 000000000..105ccd094 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-ACLHashtable.tests.ps1 @@ -0,0 +1,130 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Test to ensure the function handles the provided parameters correctly and returns the expected hashtable +Describe "ConvertTo-ACLHashtable" -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'ConvertTo-ACLHashtable.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Mock functions to simplify testing + Mock -CommandName Write-Verbose + Mock -CommandName ConvertTo-FormattedToken -MockWith { param ($Token) return $Token } + Mock -CommandName Get-BitwiseOrResult -MockWith { + param ($integers) + return $integers + } + + # Define the test cases + $referenceACLs = @( + [PSCustomObject]@{ + token = "token2" + inherited = $true + aces = @( + [PSCustomObject]@{ + permissions = @{ + allow = @{ + bit = 1 + } + deny = @{ + bit = 0 + } + } + Identity = @{ + value = @{ + ACLIdentity = @{ + descriptor = "descriptor1" + } + } + } + } + ) + } + ) + + $descriptorACLList = @( + [PSCustomObject]@{ + token = "token2" + inheritPermissions = $false + acesDictionary = @{ + descriptor2 = @{ + allow = 0 + deny = 1 + } + + } + }, + [PSCustomObject]@{ + token = "token3" + inheritPermissions = $true + acesDictionary = @{ + descriptor3 = @{ + allow = 1 + deny = 0 + } + } + } + ) + + $descriptorMatchToken = "token2" + Mock -CommandName Write-Warning + + } + + It "Correctly converts and builds the ACL hashtable" { + $result = ConvertTo-ACLHashtable -ReferenceACLs $referenceACLs -DescriptorACLList $descriptorACLList -DescriptorMatchToken $descriptorMatchToken + + $result.Count | Should -Be 2 + $result.value[0].token | Should -Be 'token3' + $result.value[0].inheritPermissions | Should -Be $true + $result.value[0].acesDictionary.descriptor3 | Should -Not -BeNullOrEmpty + $result.value[0].acesDictionary.descriptor3.allow | Should -Be 1 + $result.value[0].acesDictionary.descriptor3.deny | Should -Be 0 + + $result.value[1].token | Should -Be 'token2' + $result.value[1].inheritPermissions | Should -Be $true + $result.value[1].acesDictionary.descriptor1 | Should -Not -BeNullOrEmpty + $result.value[1].acesDictionary.descriptor1.allow | Should -Be 1 + $result.value[1].acesDictionary.descriptor1.deny | Should -Be 0 + + } + + It "Correctly converts and builds the ACL hashtable when the descriptor match token is not found" { + + $descriptorACLList = @( + [PSCustomObject]@{ + token = "token3" + inheritPermissions = $true + acesDictionary = @{ + descriptor3 = @{ + allow = 1 + deny = 0 + } + } + } + ) + + $result = ConvertTo-ACLHashtable -ReferenceACLs $referenceACLs -DescriptorACLList $descriptorACLList -DescriptorMatchToken 'token4' + + $result.Count | Should -Be 2 + $result.value[0].token | Should -Be 'token3' + $result.value[0].inheritPermissions | Should -Be $true + $result.value[1].token | Should -Be 'token2' + $result.value[1].inheritPermissions | Should -Be $true + + } +} + +# End of Pester tests for ConvertTo-ACLHashtable + +# Be sure you've installed and imported the Pester module version 5 before running this test. +# You can do so by running `Install-Module -Name Pester -Force`, then `Import-Module Pester`. diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedACL.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedACL.tests.ps1 new file mode 100644 index 000000000..fd54cdc4c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedACL.tests.ps1 @@ -0,0 +1,141 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# ConvertTo-FormattedACL.Tests.ps1 + +Describe "ConvertTo-FormattedACL" -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'ConvertTo-FormattedACL.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Find-Identity -MockWith { return @{Id = "TestIdentity"} } + Mock -CommandName Format-ACEs -MockWith { return @{ Permissions = "TestPermissions" } } + Mock -CommandName Parse-ACLToken -MockWith { return "FormattedToken" } + Mock -CommandName Write-Warning + + } + + Context "When ACL has token and ACE entries" { + + BeforeAll { + + $ACLList = @( + @{ + inheritPermissions = $true + token = "TestToken" + acesDictionary = [PSCustomObject]@{ + "TestACE" = @{ + descriptor = "TestDescriptor" + allow = 2 + deny = 0 + } + } + }, + @{ + inheritPermissions = $true + token = "TestToken" + acesDictionary = [PSCustomObject]@{ + "TestACE" = @{ + descriptor = "TestDescriptor2" + allow = 2 + deny = 0 + } + } + } + ) + + } + + It "Should format ACL Git Repositories properly" { + $result = $ACLList | ConvertTo-FormattedACL -SecurityNamespace "Git Repositories" -OrganizationName "TestOrg" + $result.count | Should -Be 2 + $result[0].token | Should -Be "FormattedToken" + $result[0].aces | Should -HaveCount 1 + $result[0].aces[0].Identity.Id | Should -Be "TestIdentity" + $result[0].aces[0].Permissions.Permissions | Should -Be "TestPermissions" + } + + It "Should format ACL Identity properly" { + $result = $ACLList | ConvertTo-FormattedACL -SecurityNamespace "Identity" -OrganizationName "TestOrg" + $result.count | Should -Be 2 + $result[0].token | Should -Be "FormattedToken" + $result[0].aces | Should -HaveCount 1 + $result[0].aces[0].Identity.Id | Should -Be "TestIdentity" + $result[0].aces[0].Permissions.Permissions | Should -Be "TestPermissions" + } + } + + Context "When ACL has no token" { + + BeforeAll { + + $ACLList = @( + @{ + inheritPermissions = $true + token = $null + acesDictionary = [PSCustomObject]@{ + "TestACE" = @{ + descriptor = "TestDescriptor" + allow = 2 + deny = 0 + } + } + }, + @{ + inheritPermissions = $true + token = "" + acesDictionary = [PSCustomObject]@{ + "TestACE" = @{ + descriptor = "TestDescriptor2" + allow = 2 + deny = 0 + } + } + } + ) + + + } + + It "Should skip ACL without token" { + $result = $ACLList | ConvertTo-FormattedACL -SecurityNamespace "Git Repositories" -OrganizationName "TestOrg" + $result | Should -BeNullOrEmpty + } + } + + Context "When ACL has empty ACE entries" { + + BeforeAll { + + $ACLList = @( + @{ + inheritPermissions = $true + token = $null + acesDictionary = [PSCustomObject]@{ + "TestACE" = @{ + descriptor = "TestDescriptor" + allow = 2 + deny = 0 + } + } + } + ) + + } + + It "Should skip ACL with empty ACE entries" { + $result = $ACLList | ConvertTo-FormattedACL -SecurityNamespace "Identity" -OrganizationName "TestOrg" + $result | Should -BeNullOrEmpty + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedToken.tests.ps1 new file mode 100644 index 000000000..b797c98e6 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/ConvertTo-FormattedToken.tests.ps1 @@ -0,0 +1,72 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# ConvertTo-FormattedToken.Tests.ps1 + +Describe "ConvertTo-FormattedToken" -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'ConvertTo-FormattedToken.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + It "should format GitOrganization token correctly" { + $token = @{ + type = 'GitOrganization' + } + + $result = ConvertTo-FormattedToken -Token $token + + $result | Should -Be 'repoV2' + } + + It "should format GitProject token correctly" { + $token = @{ + type = 'GitProject' + projectId = 'myProject' + } + + $result = ConvertTo-FormattedToken -Token $token + + $result | Should -Be 'repoV2/myProject' + } + + It "should format GitRepository token correctly" { + $token = @{ + type = 'GitRepository' + projectId = 'myProject' + RepoId = 'myRepo' + } + + $result = ConvertTo-FormattedToken -Token $token + + $result | Should -Be 'repoV2/myProject/myRepo' + } + + It "should return an empty string for unrecognized token type" { + $token = @{ + type = 'UnknownType' + } + + $result = ConvertTo-FormattedToken -Token $token + + $result | Should -Be '' + } + + It "should return an empty string for an empty token" { + $token = @{} + + $result = ConvertTo-FormattedToken -Token $token + + $result | Should -Be '' + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Format-ACEs.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Format-ACEs.tests.ps1 new file mode 100644 index 000000000..ee09895c2 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Format-ACEs.tests.ps1 @@ -0,0 +1,60 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Format-ACEs' -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Format-ACEs.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'SecurityNamespace' } -MockWith { + return @{ + Key = 'SecurityNamespace' + Type = 'SecurityNamespaces' + Actions = @( + [PSCustomObject]@{ bit = 1; Name = 'Read' }, + [PSCustomObject]@{ bit = 2; Name = 'Write' } + ) + } + } + } + + It 'Returns Allow actions from the specified security namespace' { + $result = Format-ACEs -Allow 1 -Deny 0 -SecurityNamespace "SecurityNamespace" + + $result.Allow.bit | Should -Be 1 + $result.Allow.Name | Should -Be 'Read' + $result.Deny | Should -BeNullOrEmpty + $result.DescriptorType | Should -Be "SecurityNamespace" + } + + It 'Returns Deny actions from the specified security namespace' { + $result = Format-ACEs -Allow 0 -Deny 2 -SecurityNamespace "SecurityNamespace" + + $result.Allow | Should -BeNullOrEmpty + $result.Deny.bit | Should -Be 2 + $result.Deny.Name | Should -Be 'Write' + $result.DescriptorType | Should -Be "SecurityNamespace" + } + + It 'Returns both Allow and Deny actions from the specified security namespace' { + $result = Format-ACEs -Allow 1 -Deny 2 -SecurityNamespace "SecurityNamespace" + + $result.Allow.bit | Should -Be 1 + $result.Allow.Name | Should -Be 'Read' + $result.Deny.bit | Should -Be 2 + $result.Deny.Name | Should -Be 'Write' + $result.DescriptorType | Should -Be "SecurityNamespace" + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Get-BitwiseOrResult.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Get-BitwiseOrResult.tests.ps1 new file mode 100644 index 000000000..61a4d316c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Get-BitwiseOrResult.tests.ps1 @@ -0,0 +1,60 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-BitwiseOrResult' -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-BitwiseOrResult.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + It 'should return 0 for an empty array' { + $result = Get-BitwiseOrResult -integers @() + $result | Should -Be 0 + } + + It 'should return a value for an array with a single item' { + $result = Get-BitwiseOrResult -integers 5 + $result | Should -Be 5 + } + + It 'should return the same number for a single-item array' { + $result = Get-BitwiseOrResult -integers @(5) + $result | Should -Be 5 + } + + It 'should return correct bitwise OR result for an array of positive integers' { + $result = Get-BitwiseOrResult -integers @(1, 2, 4, 8) + $result | Should -Be 15 + } + + It 'should handle large numbers correctly' { + $result = Get-BitwiseOrResult -integers @(2147483647, 1) + $result | Should -Be 2147483647 + } + + It 'should return 0 for an array with all zeros' { + $result = Get-BitwiseOrResult -integers @(0, 0, 0) + $result | Should -Be 0 + } + + It 'should write error and return null for an invalid integer' { + $invalidInteger = "abc" + { Get-BitwiseOrResult -integers @($invalidInteger) } | Should -Throw + } + + It 'should return correct result for mixture of valid integers' { + $result = Get-BitwiseOrResult -integers @(-1, 1) + $result | Should -Be -1 + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Group-ACEs.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Group-ACEs.tests.ps1 new file mode 100644 index 000000000..40c856ee9 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Group-ACEs.tests.ps1 @@ -0,0 +1,90 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Group-ACEs' -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Group-ACEs.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + $ace1 = @{ + Identity = @{ + value = @{ + originId = "user1" + } + } + Permissions = @{ + Deny = 0, 1 + Allow = 2, 3 + DescriptorType = "SecurityNamespace" + } + } + + $ace2 = @{ + Identity = @{ + value = @{ + originId = "user1" + } + } + Permissions = @{ + Deny = 0, 1 + Allow = 2, 3 + DescriptorType = "SecurityNamespace" + } + } + + $ace3 = @{ + Identity = @{ + value = @{ + originId = "user2" + } + } + Permissions = @{ + Deny = 0, 1 + Allow = 2, 3 + DescriptorType = "SecurityNamespace" + } + } + + } + + It 'Returns empty list when ACEs are not provided' { + $result = Group-ACEs -ACEs @() + $result | Should -BeNullOrEmpty + } + + It 'Processes single identity ACE' { + $result = Group-ACEs -ACEs @($ace1) + $result.Identity.value.originId | Should -Be "user1" + $result.Permissions.Deny | Should -Be 0,1 + $result.Permissions.Allow | Should -Be 2,3 + } + + It 'Groups multiple identities correctly' { + + $result = Group-ACEs -ACEs @($ace1, $ace2, $ace3) + $result.Count | Should -Be 2 + $user1 = $result | Where-Object { $_.Identity.value.originId -eq "user1" } + $user2 = $result | Where-Object { $_.Identity.value.originId -eq "user2" } + + $user1.Permissions.Deny | Should -Be 0 + $user1.Permissions.Allow | Should -Be 2 + $user2.Permissions.Deny | Should -Be 0,1 + $user2.Permissions.Allow | Should -Be 2,3 + + } + + It "Doesn't group single identity ACE" { + $result = Group-ACEs -ACEs @($ace1) + @($result).Count | Should -Be 1 + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/New-ACLToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/New-ACLToken.tests.ps1 new file mode 100644 index 000000000..a2ec3e9b4 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/New-ACLToken.tests.ps1 @@ -0,0 +1,76 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'New-ACLToken Function Tests' -Skip -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-ACLToken.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Load 001.LocalizedDataAzResourceTokenPatten + . (Get-ClassFilePath '001.LocalizedDataAzResourceTokenPatten') + + Mock -CommandName Get-CacheItem -MockWith { + return [PSCustomObject]@{id = "1234"} + } + Mock -CommandName Write-Warning + + } + + Context 'Git Repositories Namespace' { + + It 'Should return GitOrganization type for valid Git organization token' { + $result = New-ACLToken -SecurityNamespace 'Git Repositories' -TokenName 'OrgName' + $result.type | Should -Be 'GitOrganization' + } + + It 'Should return GitProject type for valid Git project token' { + $result = New-ACLToken -SecurityNamespace 'Git Repositories' -TokenName '[OrgName]/ProjectName' + $result.type | Should -Be 'GitProject' + $result.projectId | Should -Be '1234' + } + + It 'Should return GitRepository type for valid Git repository token' { + $result = New-ACLToken -SecurityNamespace 'Git Repositories' -TokenName '[OrgName]/ProjectName/RepoName' + $result.type | Should -Be 'GitRepository' + $result.projectId | Should -Be '1234' + $result.RepoId | Should -Be '1234' + } + + It 'Should return GitUnknown type for unknown Git token' { + $result = New-ACLToken -SecurityNamespace 'Git Repositories' -TokenName 'Unknown/Token' + $result.type | Should -Be 'GitUnknown' + } + } + + Context 'Identity Namespace' { + + It 'Should return GitGroupPermission type for valid identity group token' { + $result = New-ACLToken -SecurityNamespace 'Identity' -TokenName '[ProjectId]/[GroupId]' + $result.type | Should -Be 'GitGroupPermission' + $result.projectId | Should -Be 'ProjectId' + $result.groupId | Should -Be 'GroupId' + } + + It 'Should return GroupUnknown type for unknown identity token' { + $result = New-ACLToken -SecurityNamespace 'Identity' -TokenName 'Unknown/Token' + $result.type | Should -Be 'GroupUnknown' + } + } + + Context 'Unknown SecurityNamespace' { + + It 'Should return UnknownSecurityNamespace type for unrecognized security namespace' { + $result = New-ACLToken -SecurityNamespace 'Unknown' -TokenName 'Any/Token' + $result.type | Should -Be 'UnknownSecurityNamespace' + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Parse-ACLToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Parse-ACLToken.tests.ps1 new file mode 100644 index 000000000..d3c571da1 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Parse-ACLToken.tests.ps1 @@ -0,0 +1,83 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Parse-ACLToken' -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Parse-ACLToken.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + + $script:LocalizedDataAzACLTokenPatten = @{ + OrganizationGit = '^org:(.+)$' + GitProject = '^project:(.+)$' + GitRepository = '^repo:(.+)$' + GitBranch = '^branch:(.+)$' + ResourcePermission = '^resource:(.+)$' + GroupPermission = '^group:(.+)$' + } + + # If there were any Mock commands needed, they should be added here using the complete syntax. + # Example: + # Mock -CommandName SomeCommand -MockWith { + # return "mocked result" + # } + + } + + It 'Should parse OrganizationGit token correctly' { + $token = "org:testOrg" + $SecurityNamespace = "Git Repositories" + $result = Parse-ACLToken -Token $token -SecurityNamespace $SecurityNamespace + + $result.type | Should -Be 'OrganizationGit' + $result._token | Should -Be $token + } + + It 'Should parse GitProject token correctly' { + $token = "project:testProject" + $SecurityNamespace = "Git Repositories" + $result = Parse-ACLToken -Token $token -SecurityNamespace $SecurityNamespace + + $result.type | Should -Be 'GitProject' + $result._token | Should -Be $token + } + + It 'Should throw for unrecognized Git Repositories token' { + $token = "unknown:test" + $SecurityNamespace = "Git Repositories" + { Parse-ACLToken -Token $token -SecurityNamespace $SecurityNamespace } | Should -Throw "Token '$token' is not recognized." + } + + It 'Should parse ResourcePermission token correctly' { + $token = "resource:testResource" + $SecurityNamespace = "Identity" + $result = Parse-ACLToken -Token $token -SecurityNamespace $SecurityNamespace + + $result.type | Should -Be 'ResourcePermission' + $result._token | Should -Be $token + } + + It 'Should parse GroupPermission token correctly' { + $token = "group:testGroup" + $SecurityNamespace = "Identity" + $result = Parse-ACLToken -Token $token -SecurityNamespace $SecurityNamespace + + $result.type | Should -Be 'GroupPermission' + $result._token | Should -Be $token + } + + It 'Should throw for unrecognized Identity token' { + $token = "unknown:test" + $SecurityNamespace = "Identity" + { Parse-ACLToken -Token $token -SecurityNamespace $SecurityNamespace } | Should -Throw "Token '$token' is not recognized." + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Resolve-ACLToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Resolve-ACLToken.tests.ps1 new file mode 100644 index 000000000..40ef306c7 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Resolve-ACLToken.tests.ps1 @@ -0,0 +1,57 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Resolve-ACLToken' -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Resolve-ACLToken.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + $referenceObject = [PSCustomObject]@{ token = [PSCustomObject]@{ _token = 'refToken' } } + $differenceObject = [PSCustomObject]@{ token = [PSCustomObject]@{ _token = 'diffToken' } } + + # If there were any Mock commands needed, they should be added here using the complete syntax. + # Example: + # Mock -CommandName Resolve-ACLToken -MockWith { + # param ($ReferenceObject, $DifferenceObject) + # if ($DifferenceObject -ne $null) { + # return $DifferenceObject.token._token + # } + # elseif ($ReferenceObject -ne $null) { + # return $ReferenceObject.token._token + # } + # else { + # return $null + # } + # } + } + + Context 'When DifferenceObject is not null' { + It 'should return the token from DifferenceObject' { + $result = Resolve-ACLToken -ReferenceObject $referenceObject -DifferenceObject $differenceObject + $result | Should -Be 'diffToken' + } + } + + Context 'When DifferenceObject is null' { + It 'should return the token from ReferenceObject' { + $result = Resolve-ACLToken -ReferenceObject $referenceObject -DifferenceObject $null + $result | Should -Be 'refToken' + } + } + + Context 'When both DifferenceObject and ReferenceObject are null' { + It 'should return $null' { + $result = Resolve-ACLToken -ReferenceObject $null -DifferenceObject $null + $result | Should -Be $null + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Test-ACLListforChanges.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Test-ACLListforChanges.tests.ps1 new file mode 100644 index 000000000..9feba20f5 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Test-ACLListforChanges.tests.ps1 @@ -0,0 +1,279 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Test-ACLListforChanges" -Tags "Unit", "ACL", "Helper" { + + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-ACLListforChanges.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the classes to test + . (Get-ClassFilePath 'Get-BitwiseOrResult.ps1') + + # Mock Data + $ReferenceACLsSample = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 1 + } + Deny = @{ + Bit = 0 + } + } + } + ) + isInherited = $False + } + + $DifferenceACLsSample = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 1 + } + Deny = @{ + Bit = 0 + } + } + } + ) + isInherited = $False + } + + $ModifiedDifferenceACLsSample = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 2 + } + Deny = @{ + Bit = 0 + } + } + isInherited = $False + } + ) + } + + } + + It "Returns Unchanged when ACLs are identical" { + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $DifferenceACLsSample + $result.status | Should -Be "Unchanged" + } + + It "Returns Changed when ACLs are different" { + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $ModifiedDifferenceACLsSample + $result.status | Should -Be "Changed" + } + + It "Returns Missing when Reference ACL is null" { + $result = Test-ACLListforChanges -ReferenceACLs $null -DifferenceACLs $DifferenceACLsSample + $result.status | Should -Be "Missing" + } + + It "Returns NotFound when Difference ACL is null" { + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $null + $result.status | Should -Be "NotFound" + } + + It "Returns Changed when ACLs count is not equal" { + $DifferentCountACLs = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 1 + } + Deny = @{ + Bit = 0 + } + } + isInherited = $False + }, + @{ + Identity = @{ + value = @{ + originId = 2 + } + } + Permissions = @{ + Allow = @{ + Bit = 1 + } + Deny = @{ + Bit = 0 + } + } + isInherited = $False + } + ) + } + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $DifferentCountACLs + $result.status | Should -Be "Changed" + } + + It "Returns Changed when inherited flag is not equal" { + $InheritedFlagACLs = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 1 + } + Deny = @{ + Bit = 0 + } + } + } + ) + isInherited = $True + } + + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $InheritedFlagACLs + $result.status | Should -Be "Changed" + + } + + It "Returns Changed when ACE is not found in Difference ACL" { + $MissingACEACLs = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 2 + } + } + Permissions = @{ + Allow = @{ + Bit = 1 + } + Deny = @{ + Bit = 0 + } + } + } + ) + isInherited = $False + } + + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $MissingACEACLs + $result.status | Should -Be "Changed" + } + + It "Returns Changed when Allow ACEs are not equal" { + $DifferentAllowACLs = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 2 + } + Deny = @{ + Bit = 0 + } + } + } + ) + isInherited = $False + } + + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $DifferentAllowACLs + $result.status | Should -Be "Changed" + } + + It "Returns Changed when Deny ACEs are not equal" { + $DifferentDenyACLs = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 1 + } + Deny = @{ + Bit = 1 + } + } + } + ) + isInherited = $False + } + + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $DifferentDenyACLs + $result.status | Should -Be "Changed" + } + + It "Returns Changed when ACLs are different" { + $DifferentACLs = @{ + aces = @( + @{ + Identity = @{ + value = @{ + originId = 1 + } + } + Permissions = @{ + Allow = @{ + Bit = 2 + } + Deny = @{ + Bit = 1 + } + } + } + ) + isInherited = $False + } + + $result = Test-ACLListforChanges -ReferenceACLs $ReferenceACLsSample -DifferenceACLs $DifferentACLs + $result.status | Should -Be "Changed" + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiHttpRequestHeader.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiHttpRequestHeader.tests.ps1 new file mode 100644 index 000000000..055837aa7 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiHttpRequestHeader.tests.ps1 @@ -0,0 +1,68 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Test-AzDevOpsApiHttpRequestHeader' { + + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDevOpsApiHttpRequestHeader.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + + It 'Returns $true when HttpRequestHeader contains Metadata' { + $header = @{ Metadata = 'someValue' } + $result = Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header -IsValid + $result | Should -Be $true + } + + It 'Returns $true when HttpRequestHeader.Authorization is a valid Basic auth' { + $header = @{ Authorization = 'Basic: dXNlcm5hbWU6cGFzc3dvcmQ=' } + $result = Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header -IsValid + $result | Should -Be $true + } + + It 'Returns $true when HttpRequestHeader.Authorization is a valid Bearer token' { + $header = @{ Authorization = 'Bearer: yourTokenHere' } + $result = Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header -IsValid + $result | Should -Be $true + } + + It 'Returns $true when HttpRequestHeader.Authorization is a valid Bearer token with space and no colon' { + $header = @{ Authorization = 'Bearer yourTokenHere' } + $result = Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header -IsValid + $result | Should -Be $true + } + + It 'Returns $false when HttpRequestHeader.Authorization is null' { + $header = @{ Authorization = $null } + $result = Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header -IsValid + $result | Should -Be $false + } + + It 'Returns $false when HttpRequestHeader is null' { + $header = $null + $result = Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header -IsValid + $result | Should -Be $false + } + + It 'Returns $false when HttpRequestHeader.Authorization is invalid' { + $header = @{ Authorization = 'InvalidAuthString' } + $result = Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header -IsValid + $result | Should -Be $false + } + + It 'Throws exception when IsValid switch is missing' -skip { + $header = @{ Authorization = 'Bearer: yourTokenHere' } + { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $header } | Should -Throw + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiResourceId.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiResourceId.tests.ps1 new file mode 100644 index 000000000..f29b05609 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiResourceId.tests.ps1 @@ -0,0 +1,36 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Test-AzDevOpsApiResourceId' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDevOpsApiResourceId.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + It 'Returns $true for a valid ResourceId' { + $ValidResourceId = [guid]::NewGuid().ToString() + $result = Test-AzDevOpsApiResourceId -ResourceId $ValidResourceId -IsValid + $result | Should -Be $true + } + + It 'Returns $false for an invalid ResourceId' { + $InvalidResourceId = 'Invalid-GUID' + $result = Test-AzDevOpsApiResourceId -ResourceId $InvalidResourceId -IsValid + $result | Should -Be $false + } + + It 'Throws exception if IsValid switch is not provided' -skip { + $ValidResourceId = [guid]::NewGuid().ToString() + { Test-AzDevOpsApiResourceId -ResourceId $ValidResourceId } | Should -Throw + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiTimeoutExceeded.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiTimeoutExceeded.tests.ps1 new file mode 100644 index 000000000..d1723909e --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiTimeoutExceeded.tests.ps1 @@ -0,0 +1,48 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Test-AzDevOpsApiTimeoutExceeded" { + + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Test-AzDevOpsApiTimeoutExceeded.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + } + + It "Should return true if duration exceeds TimeoutMs" { + $StartTime = [datetime]::Now + $EndTime = $StartTime.AddMilliseconds(1200) + $TimeoutMs = 1000 + $result = Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs + $result | Should -Be $true + } + + It "Should return false if duration does not exceed TimeoutMs" { + $StartTime = [datetime]::Now + $EndTime = $StartTime.AddMilliseconds(800) + $TimeoutMs = 1000 + $result = Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs + $result | Should -Be $false + } + + It "Should validate the TimeoutMs parameter against its range" { + $StartTime = [datetime]::Now + $EndTime = $StartTime.AddMilliseconds(3000) + $TimeoutMs = 200000 + {Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs} | Should -Not -Throw + } + + It "Should return false if duration equals TimeoutMs" { + $StartTime = [datetime]::Now + $EndTime = $StartTime.AddMilliseconds(1000) + $TimeoutMs = 1000 + $result = Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs + $result | Should -Be $false + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiUri.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiUri.tests.ps1 new file mode 100644 index 000000000..65306952f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiUri.tests.ps1 @@ -0,0 +1,46 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Test-AzDevOpsApiUri' { + + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDevOpsApiUri.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + } + + It 'Returns $true if ApiUri is null or empty' { + $result = Test-AzDevOpsApiUri -ApiUri '' -IsValid + $result | Should -Be $true + } + + It 'Returns $true for a valid HTTP URI when -IsValid is used' { + $result = Test-AzDevOpsApiUri -ApiUri 'http://example.com/_apis/' -IsValid + $result | Should -Be $true + } + + It 'Returns $true for a valid HTTPS URI when -IsValid is used' { + $result = Test-AzDevOpsApiUri -ApiUri 'https://example.com/_apis/' -IsValid + $result | Should -Be $true + } + + It 'Returns $false for an invalid HTTP URI when -IsValid is used' { + $result = Test-AzDevOpsApiUri -ApiUri 'http://example.com/invalid' -IsValid + $result | Should -Be $false + } + + It 'Returns $false for an invalid HTTPS URI when -IsValid is used' { + $result = Test-AzDevOpsApiUri -ApiUri 'https://example.com/invalid' -IsValid + $result | Should -Be $false + } + + It 'Throws an exception if -IsValid is not used' -skip { + { Test-AzDevOpsApiUri -ApiUri 'http://example.com/_apis/' } | Should -Throw + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiVersion.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiVersion.tests.ps1 new file mode 100644 index 000000000..3cab3d9f3 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Test-AzDevOpsApiVersion.tests.ps1 @@ -0,0 +1,28 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Test-AzDevOpsApiVersion' { + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDevOpsApiVersion.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + } + + It 'Should return $true for supported API version' { + $result = Test-AzDevOpsApiVersion -ApiVersion '6.0' -IsValid + $result | Should -Be $true + } + + It 'Should return $false for unsupported API version' { + $result = Test-AzDevOpsApiVersion -ApiVersion '5.0' -IsValid + $result | Should -Be $false + } + +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Wait-AzDevOpsApiResource.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Wait-AzDevOpsApiResource.tests.ps1 new file mode 100644 index 000000000..6d670dd67 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/API/Wait-AzDevOpsApiResource.tests.ps1 @@ -0,0 +1,82 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Wait-AzDevOpsApiResource' -skip { + + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Wait-AzDevOpsApiResource.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName 'Test-AzDevOpsApiUri' -MockWith { return $true } + Mock -CommandName 'Test-AzDevOpsApiVersion' -MockWith { return $true } + Mock -CommandName 'Test-AzDevOpsPat' -MockWith { return $true } + Mock -CommandName 'Test-AzDevOpsApiResourceName' -MockWith { return $true } + Mock -CommandName 'Test-AzDevOpsApiResourceId' -MockWith { return $true } + Mock -CommandName 'Get-AzDevOpsApiVersion' -MockWith { return '6.0' } + Mock -CommandName 'Get-AzDevOpsApiWaitIntervalMs' -MockWith { return 1000 } + Mock -CommandName 'Get-AzDevOpsApiWaitTimeoutMs' -MockWith { return 30000 } + Mock -CommandName 'Test-AzDevOpsApiResource' -MockWith { return $false } + Mock -CommandName 'Test-AzDevOpsApiTimeoutExceeded' -MockWith { return $false } + Mock -CommandName 'New-InvalidOperationException' -MockWith { Throw "Operation Timeout" } + + } + + Context 'When waiting for resource to be present' { + It 'Waits for the resource to be present' { + $script:localizedData = @{ + AzDevOpsApiResourceWaitTimeoutExceeded = 'Timeout exceeded waiting for {0} resource {1} with ID {2} after {3} milliseconds.' + } + $params = @{ + ApiUri = 'https://dev.azure.com/example/_apis/' + Pat = 'dummyPAT' + ResourceName = 'Project' + ResourceId = 'dummyResourceId' + IsPresent = $true + WaitIntervalMilliseconds = 1000 + WaitTimeoutMilliseconds = 5000 + } + + { Wait-AzDevOpsApiResource @params } | Should -Not -Throw + } + } + + Context 'When waiting for resource to be absent' { + It 'Waits for the resource to be absent' { + Mock -CommandName 'Test-AzDevOpsApiResource' -MockWith { return $true } + $script:localizedData = @{ + AzDevOpsApiResourceWaitTimeoutExceeded = 'Timeout exceeded waiting for {0} resource {1} with ID {2} after {3} milliseconds.' + } + $params = @{ + ApiUri = 'https://dev.azure.com/example/_apis/' + Pat = 'dummyPAT' + ResourceName = 'Project' + ResourceId = 'dummyResourceId' + IsAbsent = $true + WaitIntervalMilliseconds = 1000 + WaitTimeoutMilliseconds = 5000 + } + + { Wait-AzDevOpsApiResource @params } | Should -Not -Throw + } + } + + Context 'When both IsPresent and IsAbsent are missing' { + It 'Throws an error' { + $params = @{ + ApiUri = 'https://dev.azure.com/example/_apis/' + Pat = 'dummyPAT' + ResourceName = 'Project' + ResourceId = 'dummyResourceId' + } + + { Wait-AzDevOpsApiResource @params } | Should -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ConvertTo-Base64String.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ConvertTo-Base64String.tests.ps1 new file mode 100644 index 000000000..42ff9e0ae --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ConvertTo-Base64String.tests.ps1 @@ -0,0 +1,54 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "ConvertTo-Base64String" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "ConvertTo-Base64String.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + It "should convert a string to a Base64 string" { + $input = "Hello, World!" + $expected = "SGVsbG8sIFdvcmxkIQ==" + + $result = ConvertTo-Base64String -InputObject $input + + $result | Should -Be $expected + } + + It "should throw an error for null input" { + { ConvertTo-Base64String -InputObject $null } | Should -Throw + } + + It "should throw an error for empty input" { + { ConvertTo-Base64String -InputObject "" } | Should -Throw + } + + It "should handle special characters correctly" { + $input = "!@#$%^&*()_+|" + $expected = "IUAjJCVeJiooKV8rfA==" + + $result = ConvertTo-Base64String -InputObject $input + + $result | Should -Be $expected + } + + It "should handle non-ASCII characters correctly" { + $input = "こんにちは" + $expected = "44GT44KT44Gr44Gh44Gv" + + $result = ConvertTo-Base64String -InputObject $input + + $result | Should -Be $expected + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-AzDoIdentity.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-AzDoIdentity.tests.ps1 new file mode 100644 index 000000000..7481edb56 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-AzDoIdentity.tests.ps1 @@ -0,0 +1,185 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Find-AzDoIdentity" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Find-AzDoIdentity.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + . (Get-ClassFilePath '000.CacheItem') + + # Mocking Get-CacheItem to simulate cache retrieval + Mock -CommandName Get-CacheItem -MockWith { + param ($Key, $Type) + + switch ($Type) + { + 'LiveUsers' + { + if ($Key -eq 'user@domain.com') + { + return @{ + ACLIdentity = @{ + descriptor = "userDescriptor" + id = "userId" + } + originId = "userOriginId" + principalName = "userPrincipalName" + displayName = "User Display Name" + } + } + return $null + } + 'LiveGroups' + { + if ($Key -eq '[Project]\GroupName') + { + return @{ + ACLIdentity = @{ + descriptor = "groupDescriptor" + id = "groupId" + } + originId = "groupOriginId" + principalName = "groupPrincipalName" + displayName = "Group Display Name" + } + } + return $null + } + } + } + + Mock Write-Verbose + Mock Write-Warning + + $Global:AZDOLiveUsers = @( + @{ + key = "user1" + value = @{ + ACLIdentity = @{ + descriptor = "descriptor1" + id = "id1" + } + originId = "originId1" + principalName = "principalName1" + displayName = "User One" + } + }, + @{ + key = "user2" + value = @{ + ACLIdentity = @{ + descriptor = "descriptor2" + id = "id2" + } + originId = "originId2" + principalName = "principalName2" + displayName = "User Two" + } + } + ) + + $Global:AZDOLiveGroups = @( + @{ + key = "group1" + value = @{ + ACLIdentity = @{ + descriptor = "descriptor1" + id = "id1" + } + originId = "originId1" + principalName = "principalName1" + displayName = "Group One" + } + }, + @{ + key = "group2" + value = @{ + ACLIdentity = @{ + descriptor = "descriptor2" + id = "id2" + } + originId = "originId2" + principalName = "principalName2" + displayName = "Group Two" + } + } + ) + } + + It "Should find user by email address" { + $result = Find-AzDoIdentity -Identity 'user@domain.com' + $result.ACLIdentity.descriptor | Should -Be 'userDescriptor' + } + + It "Should find group by name with backslash" { + $result = Find-AzDoIdentity -Identity 'Project\GroupName' + $result.ACLIdentity.descriptor | Should -Be 'groupDescriptor' + } + + It "Should find group by name with forward slash" { + $result = Find-AzDoIdentity -Identity 'Project/GroupName' + $result.ACLIdentity.descriptor | Should -Be 'groupDescriptor' + } + + It "Should find user by display name" { + $result = Find-AzDoIdentity -Identity 'User One' + $result.displayName | Should -Be 'User One' + } + + It "Should find group by display name" { + $result = Find-AzDoIdentity -Identity 'Group One' + $result.displayName | Should -Be 'Group One' + } + + It "Should handle multiple users with the same display name" { + Mock -CommandName 'Where-Object' -MockWith { + param ($condition) + return @($Global:AZDOLiveUsers[0], $Global:AZDOLiveUsers[1]) + } + $result = Find-AzDoIdentity -Identity 'User One' + $result | Should -BeNullOrEmpty + } + + It "Should handle multiple groups with the same display name" { + Mock -CommandName 'Where-Object' -MockWith { + param ($condition) + return @($Global:AZDOLiveGroups[0], $Global:AZDOLiveGroups[1]) + } + + $result = Find-AzDoIdentity -Identity 'Group One' + $result | Should -BeNullOrEmpty + } + + It "Should handle both user and group with the same display name" { + Mock -CommandName 'Where-Object' -MockWith { + param ($condition) + if ($condition -like "*User One*") { + return $Global:AZDOLiveUsers[0] + } elseif ($condition -like "*Group One*") { + return $Global:AZDOLiveGroups[0] + } + } + + $result = Find-AzDoIdentity -Identity 'User One' + $result | Should -BeNullOrEmpty + } + + It "Should return null if no identity found" { + $result = Find-AzDoIdentity -Identity 'NonExistent' + $result | Should -BeNullOrEmpty + } + + It "Should throw identity is null" { + { Find-AzDoIdentity -Identity $null } | Should -Throw + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-Identity.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-Identity.tests.ps1 new file mode 100644 index 000000000..b6081be97 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Find-Identity.tests.ps1 @@ -0,0 +1,158 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Unit Tests for Find-Identity function +Describe 'Find-Identity Function Tests' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Find-Identity.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Mock Get-CacheObject to return test data + Mock -CommandName Get-CacheObject -MockWith { + param ( + [string]$CacheType + ) + + switch ($CacheType) + { + 'LiveGroups' { + return @{ value = [PSCustomObject]@{ ACLIdentity = [PSCustomObject]@{ descriptor = 'groupDescriptor'; id = 'groupId'; originId = 'groupOrigin'; principalName = 'groupPrincipal'; displayName = 'groupDisplay' } } } + } + 'LiveUsers' { + return @{ value = [PSCustomObject]@{ ACLIdentity = [PSCustomObject]@{ descriptor = 'userDescriptor'; id = 'userId'; originId = 'userOrigin'; principalName = 'userPrincipal'; displayName = 'userDisplay' } } } + } + 'LiveServicePrinciples' { + return @{ value = [PSCustomObject]@{ ACLIdentity = [PSCustomObject]@{ descriptor = 'spDescriptor'; id = 'spId'; originId = 'spOrigin'; principalName = 'spPrincipal'; displayName = 'spDisplay' } } } + } + } + } + + # Mock Get-DevOpsDescriptorIdentity to return test identity + Mock -CommandName Get-DevOpsDescriptorIdentity -MockWith { + param ( + [string]$OrganizationName, + [string]$Descriptor + ) + + return [PSCustomObject]@{ ACLIdentity = [PSCustomObject]@{ descriptor = 'apiDescriptor'; id = 'apiId'; originId = 'apiOrigin'; principalName = 'apiPrincipal'; displayName = 'apiDisplay' } } + } + + Mock Write-Verbose + Mock Write-Warning + + } + + Context "when searching the existing cache" { + + It 'Should return group identity for valid group descriptor' { + $result = Find-Identity -Name 'groupDescriptor' -OrganizationName 'TestOrg' -SearchType 'descriptor' + + $result.value.ACLIdentity.descriptor | Should -Be 'groupDescriptor' + } + + It 'Should return user identity for valid user descriptor' { + $result = Find-Identity -Name 'userDescriptor' -OrganizationName 'TestOrg' -SearchType 'descriptor' + + $result.value.ACLIdentity.descriptor | Should -Be 'userDescriptor' + } + + It 'Should return null for multiple identities with the same name' { + Mock -CommandName Get-CacheObject -MockWith { + @{ + value = [PSCustomObject]@{ ACLIdentity = [PSCustomObject]@{ descriptor = 'duplicateDescriptor'; id = 'duplicateId' } } + }, @{ + value = [PSCustomObject]@{ ACLIdentity = [PSCustomObject]@{ descriptor = 'duplicateDescriptor'; id = 'duplicateId' } } + } + } + + $result = Find-Identity -Name 'duplicateDescriptor' -OrganizationName 'TestOrg' -SearchType 'descriptor' + + $result | Should -BeNullOrEmpty + } + + } + + Context "when searching the cache with a known name" { + + BeforeAll { + + $params = @( + @{ + SearchType = 'descriptor' + } + @{ + SearchType = 'descriptorId' + } + @{ + SearchType = 'originId' + } + @{ + SearchType = 'principalName' + } + @{ + SearchType = 'displayName' + } + ) + + } + + it 'Should write a terminating error when the SearchType is incorrect' { + { Find-Identity -Name 'groupDescriptor' -OrganizationName 'TestOrg' -SearchType 'invalidType' } | Should -Throw + } + + it 'Should return a value for the search-type ' -TestCases $params { + param ( + [string]$SearchType + ) + + $result = Find-Identity -Name 'groupDescriptor' -OrganizationName 'TestOrg' -SearchType $SearchType + $result | Should -Not -BeNullOrEmpty + + } + + } + + Context "when searching the API" { + + It 'Should return null for non-existent descriptor' { + + Mock -CommandName Get-DevOpsDescriptorIdentity -MockWith { + return $null + } + + $result = Find-Identity -Name 'nonExistentDescriptor' -OrganizationName 'TestOrg' -SearchType 'descriptor' + $result | Should -BeNullOrEmpty + } + + It 'Should attempt to search the cache again using the ID this time' { + Mock Write-Warning -Verifiable + Mock Write-Verbose -Verifiable + Mock -CommandName Get-DevOpsDescriptorIdentity -MockWith { + return @{ + id = 'groupId' + descriptor = 'mockDescriptor' + } + } + + $result = Find-Identity -Name 'unknownName' -OrganizationName 'TestOrg' -SearchType 'descriptor' + + Assert-MockCalled Get-DevOpsDescriptorIdentity + $result.value.ACLIdentity.descriptor | Should -Be 'groupDescriptor' + Assert-VerifiableMock + } + + } + + + + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroup.tests.ps1 new file mode 100644 index 000000000..e22b28db9 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoGroup.tests.ps1 @@ -0,0 +1,63 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Format-AzDoGroup' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Format-AzDoGroup.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + Context 'Formatting UPN' { + It 'Should format correctly with valid inputs' { + Mock -CommandName Format-AzDoGroup -MockWith { + param ( + [string]$Prefix, + [string]$GroupName + ) + + return '[{0}]\{1}' -f $Prefix.Trim('[]'), $GroupName + } + + $result = Format-AzDoGroup -Prefix "Contoso" -GroupName "Developers" + $result | Should -Be "[Contoso]\Developers" + } + + It 'Should remove starting/ending square brackets from Prefix' { + Mock -CommandName Format-AzDoGroup -MockWith { + param ( + [string]$Prefix, + [string]$GroupName + ) + + return '[{0}]\{1}' -f $Prefix.Trim('[]'), $GroupName + } + + $result = Format-AzDoGroup -Prefix "[Contoso]" -GroupName "Developers" + $result | Should -Be "[Contoso]\Developers" + } + + It 'Should remove starting/ending square brackets from Prefix' { + Mock -CommandName Format-AzDoGroup -MockWith { + param ( + [string]$Prefix, + [string]$GroupName + ) + + return '[{0}]\{1}' -f $Prefix.Trim('[]'), $GroupName + } + + $result = Format-AzDoGroup -Prefix "[Contoso]" -GroupName "Developers" + $result | Should -Be "[Contoso]\Developers" + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoProjectName.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoProjectName.tests.ps1 new file mode 100644 index 000000000..fab0b1bbe --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-AzDoProjectName.tests.ps1 @@ -0,0 +1,59 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Format-AzDoProjectName' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Format-AzDoProjectName.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Write-Verbose + + } + + Context 'When GroupName is already formatted' { + It 'Returns the same GroupName' { + $result = Format-AzDoProjectName -GroupName '[ProjectName]\GroupName' -OrganizationName 'OrgName' + $result | Should -Be '[ProjectName]\GroupName' + } + } + + Context 'When GroupName needs formatting' { + It 'Formats correctly with given organization name' { + $result = Format-AzDoProjectName -GroupName 'ProjectName/GroupName' -OrganizationName 'OrgName' + $result | Should -Be '[ProjectName]\GroupName' + } + + It 'Throws if the given format has insufficient parts' { + { Format-AzDoProjectName -GroupName 'GroupName' -OrganizationName 'OrgName' } | Should -Throw + } + + It 'Replaces %ORG% with given organization name' { + $result = Format-AzDoProjectName -GroupName '%ORG%\GroupName' -OrganizationName 'OrgName' + $result | Should -Be '[OrgName]\GroupName' + } + + It 'Replaces %TFS% with TEAM FOUNDATION' { + $result = Format-AzDoProjectName -GroupName '%TFS%\GroupName' -OrganizationName 'OrgName' + $result | Should -Be '[TEAM FOUNDATION]\GroupName' + } + + It 'Throws if group part is empty' { + { Format-AzDoProjectName -GroupName 'ProjectName\' -OrganizationName 'OrgName' } | Should -Throw + } + + It 'Trims leading and trailing spaces' { + $result = Format-AzDoProjectName -GroupName ' ProjectName / GroupName ' -OrganizationName 'OrgName' + $result | Should -Be '[ProjectName]\GroupName' + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-DescriptorType.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-DescriptorType.tests.ps1 new file mode 100644 index 000000000..bff737772 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Format-DescriptorType.tests.ps1 @@ -0,0 +1,36 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Format-DescriptorType' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Format-DescriptorType.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + + It 'returns "Git Repositories" for DescriptorType "GitRepositories"' { + $result = Format-DescriptorType -DescriptorType 'GitRepositories' + $result | Should -Be 'Git Repositories' + } + + It 'returns the same value for DescriptorType "APIServices"' { + $result = Format-DescriptorType -DescriptorType 'APIServices' + $result | Should -Be 'APIServices' + } + + It 'returns the same value for DescriptorType "Webhooks"' { + $result = Format-DescriptorType -DescriptorType 'Webhooks' + $result | Should -Be 'Webhooks' + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiHttpRequestHeader.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiHttpRequestHeader.tests.ps1 new file mode 100644 index 000000000..00963ac3b --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiHttpRequestHeader.tests.ps1 @@ -0,0 +1,52 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-AzDevOpsApiHttpRequestHeader' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Get-AzDevOpsApiHttpRequestHeader.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + Mock -CommandName Test-AzDevOpsPat -MockWith { + param ( + [string]$Pat + ) + return $true + } + + } + + Context 'when called with valid PAT' { + It 'should return a hashtable with Authorization header' { + $Pat = 'ValidPAT' + $ExpectedHeader = @{ + Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$Pat")) + } + + $Result = Get-AzDevOpsApiHttpRequestHeader -Pat $Pat + + $Result | Should -BeOfType 'Hashtable' + $Result['Authorization'] | Should -BeExactly $ExpectedHeader['Authorization'] + } + } + + Context 'when called with invalid PAT' { + It 'should throw a validation exception' { + + Mock -CommandName Test-AzDevOpsPat -MockWith { + return $false + } + + { Get-AzDevOpsApiHttpRequestHeader -Pat 'InvalidPAT' } | Should -Throw + + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceName.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceName.tests.ps1 new file mode 100644 index 000000000..3e4201975 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceName.tests.ps1 @@ -0,0 +1,24 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-AzDevOpsApiResourceName' { + + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Get-AzDevOpsApiResourceName.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + } + + It 'Should return expected resource names' { + $expected = @('Operation', 'Project') + $result = Get-AzDevOpsApiResourceName + $result | Should -Be $expected + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceUri.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceUri.tests.ps1 new file mode 100644 index 000000000..413660662 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiResourceUri.tests.ps1 @@ -0,0 +1,46 @@ +$currentFile = $MyInvocation.MyCommand.Path +# Not used +Describe 'Get-AzDevOpsApiResourceUri' -skip { + Mock Get-AzDevOpsApiVersion { return '6.0' } + Mock Test-AzDevOpsApiUri { return $true } + Mock Test-AzDevOpsApiVersion { return $true } + Mock Test-AzDevOpsApiResourceName { return $true } + Mock Test-AzDevOpsApiResourceId { return $true } + Mock Get-AzDevOpsApiUriAreaName { param($ResourceName) return 'defaultarea' } + Mock Get-AzDevOpsApiUriResourceName { param($ResourceName) return $ResourceName } + + Context 'When called with mandatory parameters only' { + It 'Should return a valid URI with default API version' { + $result = Get-AzDevOpsApiResourceUri -ApiUri 'https://dev.azure.com/someOrg/_apis/' -ResourceName 'Project' + $expectedUri = 'https://dev.azure.com/someOrg/_apis/defaultarea/Project/?&api-version=6.0&includeCapabilities=true' + $result | Should -Be $expectedUri + } + } + + Context 'When called with ResourceId' { + It 'Should include the ResourceId in the returned URI' { + $result = Get-AzDevOpsApiResourceUri -ApiUri 'https://dev.azure.com/someOrg/_apis/' -ResourceName 'Project' -ResourceId '12345' + $expectedUri = 'https://dev.azure.com/someOrg/_apis/defaultarea/Project/12345/?&api-version=6.0&includeCapabilities=true' + $result | Should -Be $expectedUri + } + } + + Context 'When called with custom ApiVersion' { + It 'Should include the custom ApiVersion in the returned URI' { + $result = Get-AzDevOpsApiResourceUri -ApiUri 'https://dev.azure.com/someOrg/_apis/' -ResourceName 'Project' -ApiVersion '5.0' + $expectedUri = 'https://dev.azure.com/someOrg/_apis/defaultarea/Project/?&api-version=5.0&includeCapabilities=true' + $result | Should -Be $expectedUri + } + } + + Context 'When ResourceName is in core area' { + Mock Get-AzDevOpsApiUriAreaName { param($ResourceName) return 'core' } + + It 'Should not append area name to URI' { + $result = Get-AzDevOpsApiResourceUri -ApiUri 'https://dev.azure.com/someOrg/_apis/' -ResourceName 'Project' + $expectedUri = 'https://dev.azure.com/someOrg/_apis/Project/?&api-version=6.0&includeCapabilities=true' + $result | Should -Be $expectedUri + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriAreaName.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriAreaName.tests.ps1 new file mode 100644 index 000000000..3507b6789 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriAreaName.tests.ps1 @@ -0,0 +1,46 @@ +$currentFile = $MyInvocation.MyCommand.Path +# Not used +Describe 'Get-AzDevOpsApiUriAreaName' -skip { + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Get-AzDevOpsApiUriAreaName.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + Context 'When ResourceName is provided and valid' { + It 'Should return corresponding URI-specific area name' { + $result = Get-AzDevOpsApiUriAreaName -ResourceName 'Project' + $result | Should -Be 'core' + } + + It 'Should return another URI-specific area name' { + $result = Get-AzDevOpsApiUriAreaName -ResourceName 'Profile' + $result | Should -Be 'profile' + } + } + + Context 'When no ResourceName is provided' { + It 'Should return all unique URI-specific area names' { + $result = Get-AzDevOpsApiUriAreaName + $result | Should -Contain 'core' + $result | Should -Contain 'profile' + $result.Count | Should -Be 2 + } + } + + Context 'When invalid ResourceName is provided' { + It 'Should throw validation exception' { + { Get-AzDevOpsApiUriAreaName -ResourceName 'Invalid' } | Should -Throw + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriResourceName.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriResourceName.tests.ps1 new file mode 100644 index 000000000..50f13b795 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiUriResourceName.tests.ps1 @@ -0,0 +1,45 @@ +$currentFile = $MyInvocation.MyCommand.Path +# Ignore the file. It is not used. +Describe 'Get-AzDevOpsApiUriResourceName Tests' -skip { + + BeforeEach { + function Test-AzDevOpsApiResourceName { + param ($ResourceName, $IsValid) + return $true + } + } + + Context 'When ResourceName is provided' { + It 'Should return correct URI-specific resource name for "Project"' { + $result = Get-AzDevOpsApiUriResourceName -ResourceName 'Project' + $result | Should -Be 'projects' + } + + It 'Should return correct URI-specific resource name for "Operation"' { + $result = Get-AzDevOpsApiUriResourceName -ResourceName 'Operation' + $result | Should -Be 'operations' + } + } + + Context 'When ResourceName is not provided' { + It 'Should return all URI-specific resource names' { + $result = Get-AzDevOpsApiUriResourceName + $expectedResult = @('operations', 'projects') + $result | Should -Be $expectedResult + } + } + + Context 'When ResourceName is invalid' { + BeforeEach { + function Test-AzDevOpsApiResourceName { + param ($ResourceName, $IsValid) + return $false + } + } + + It 'Should not validate and throw an error' { + { Get-AzDevOpsApiUriResourceName -ResourceName 'InvalidResource' } | Should -Throw + } + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiVersion.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiVersion.tests.ps1 new file mode 100644 index 000000000..8fa65953f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiVersion.tests.ps1 @@ -0,0 +1,31 @@ +$currentFile = $MyInvocation.MyCommand.Path +Describe 'Get-AzDevOpsApiVersion Tests' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Get-AzDevOpsApiVersion.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + It 'Should return all supported API versions when no parameters are specified' { + $result = Get-AzDevOpsApiVersion + $result.count | Should -BeGreaterThan 1 + } + + It 'Should return default API version when -Default is specified' { + $expected = '7.0-preview.1' + $result = Get-AzDevOpsApiVersion -Default + $result | Should -Be $expected + } + +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitIntervalMs.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitIntervalMs.tests.ps1 new file mode 100644 index 000000000..8bc920466 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitIntervalMs.tests.ps1 @@ -0,0 +1,28 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Get-AzDevOpsApiWaitIntervalMs" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Get-AzDevOpsApiWaitIntervalMs.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + } + + It "Should return an integer" { + $result = Get-AzDevOpsApiWaitIntervalMs + $result | Should -BeOfType 'System.Int32' + } + + It "Should return 500" { + $result = Get-AzDevOpsApiWaitIntervalMs + $result | Should -Be 500 + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitTimeoutMs.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitTimeoutMs.tests.ps1 new file mode 100644 index 000000000..7ca5d8e42 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDevOpsApiWaitTimeoutMs.tests.ps1 @@ -0,0 +1,23 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Get-AzDevOpsApiWaitTimeoutMs" { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Get-AzDevOpsApiWaitTimeoutMs.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + } + + It "Should return 300000 milliseconds" { + $result = Get-AzDevOpsApiWaitTimeoutMs + $result | Should -Be 300000 + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDoCacheObjects.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDoCacheObjects.tests.ps1 new file mode 100644 index 000000000..27b5bf601 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Get-AzDoCacheObjects.tests.ps1 @@ -0,0 +1,43 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-AzDoCacheObjects' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Get-AzDoCacheObjects.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + } + + It 'Returns an array with 13 elements' { + $result = Get-AzDoCacheObjects + $result.Length | Should -Be 13 + } + + It 'Contains expected elements' { + $expectedElements = @( + 'Project', + 'Team', + 'Group', + 'SecurityDescriptor', + 'LiveGroups', + 'LiveProjects', + 'LiveUsers', + 'LiveGroupMembers', + 'LiveRepositories', + 'LiveServicePrinciples', + 'LiveACLList', + 'LiveProcesses', + 'SecurityNamespaces' + ) + $result = Get-AzDoCacheObjects + $result | Should -Be $expectedElements + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Invoke-AzDevOpsApiRestMethod.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Invoke-AzDevOpsApiRestMethod.tests.ps1 new file mode 100644 index 000000000..58ece83a6 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/Invoke-AzDevOpsApiRestMethod.tests.ps1 @@ -0,0 +1,179 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Invoke-AzDevOpsApiRestMethod' { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-AzDevOpsApiRestMethod.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + # Get 007.APIRateLimit.ps1 + . (Get-ClassFilePath '007.APIRateLimit') + + $defaultParameters = @{ + ApiUri = 'https://dev.azure.com/someOrganizationName/_apis/' + HttpMethod = 'Get' + HttpHeaders = @{} + RetryAttempts = 1 + RetryIntervalMs = 250 + } + + Mock -CommandName Test-AzDevOpsApiHttpRequestHeader -MockWith { return $true } + Mock -CommandName Get-AzDevOpsApiVersion -MockWith { return '6.0-preview.1' } + Mock -CommandName Add-AuthenticationHTTPHeader -MockWith { return $null } + + # Define a custom exception class + class CustomException : System.Exception { + + [System.Net.WebExceptionStatus]$Status + [HashTable]$Response + + CustomException([string]$message, [System.Net.WebExceptionStatus]$status, + [HashTable]$httpWebResponse, [System.Net.HttpStatusCode]$statusCode) : base($message) { + $this.Status = $status + $this.Response = @{ + StatusCode = $statusCode + Headers = $httpWebResponse + } + } + } + + } + + Context 'Basic functionality' { + + BeforeAll { + Mock -CommandName Invoke-RestMethod -MockWith { + param ( + [string]$Uri, + [string]$Method, + [hashtable]$Headers + ) + # Default mock behavior can be defined here if needed. + } + } + + It 'should call Invoke-RestMethod with correct parameters' { + Invoke-AzDevOpsApiRestMethod @defaultParameters + Assert-MockCalled -CommandName Invoke-RestMethod -Exactly -Times 1 + } + + It 'should return results from Invoke-RestMethod' { + Mock -CommandName Invoke-RestMethod -MockWith { return @{ success = $true } } + $result = Invoke-AzDevOpsApiRestMethod @defaultParameters + $result | Should -BeOfType [System.Collections.Hashtable] + $result.success | Should -Be $true + } + } + + Context 'Retry mechanism' { + It 'should retry if Invoke-RestMethod throws' { + Mock -CommandName Start-Sleep + Mock -CommandName Invoke-RestMethod -MockWith { throw "Error" } + $parameters = $defaultParameters.Clone() + $parameters.RetryAttempts = 2 + + { Invoke-AzDevOpsApiRestMethod @parameters } | Should -Throw + Assert-MockCalled -CommandName Invoke-RestMethod -Exactly -Times 3 + } + + It 'should wait between retries' { + Mock -CommandName Start-Sleep -Verifiable + Mock -CommandName Invoke-RestMethod -MockWith { throw "Error" } + $parameters = $defaultParameters.Clone() + $parameters.RetryAttempts = 2 + + { Invoke-AzDevOpsApiRestMethod @parameters } | Should -Throw + Assert-MockCalled -CommandName Start-Sleep -Exactly -Times 3 + } + } + + Context 'Continuation token handling' { + + AfterAll { + Remove-Variable -Name ResponseHeaders -Scope Global -ErrorAction SilentlyContinue + } + + It 'should handle continuation tokens and loop until no token is found' { + + # First call + Mock -CommandName Invoke-RestMethod -ParameterFilter { $Uri -notlike '*continuationToken*' } -MockWith { + Set-Variable responseHeaders -Value @{ 'x-ms-continuationtoken' = 'token' } -Scope Global + return @{ success = $true } + } -Verifiable + + # Second call + Mock -CommandName Invoke-RestMethod -ParameterFilter { $Uri -like '*continuationToken*' } -MockWith { + Remove-Variable -Name ResponseHeaders -Scope Global -ErrorAction SilentlyContinue + return @{ success = $true } + } -Verifiable + + $parameters = $defaultParameters.Clone() + $result = Invoke-AzDevOpsApiRestMethod @parameters + + Assert-MockCalled -CommandName Invoke-RestMethod -Times 2 + $result | Should -BeOfType [System.Collections.Hashtable] + $result.Count | Should -Be 2 + } + } + + Context 'HTTP 429 Handling' { + + AfterAll { + Remove-Variable -Name TooManyRequestsFlag -Scope Global -ErrorAction SilentlyContinue + Remove-Variable -Name DSCAZDO_APIRateLimit -Scope Global -ErrorAction SilentlyContinue + } + + It 'should handle HTTP 429 and retry with appropriate delay' { + + Mock -CommandName Write-Verbose + Mock -CommandName Write-Warning + + Mock -CommandName Invoke-RestMethod -MockWith { + Set-Variable TooManyRequestsFlag -Value $true -Scope Global + + Throw [CustomException]::New( + "Too Many Requests", + [System.Net.WebExceptionStatus]::ProtocolError, + @{ "Retry-After" = 1 }, + [System.Net.HttpStatusCode]::TooManyRequests + ) + + } -ParameterFilter { + $null -eq $Global:TooManyRequestsFlag + } + + Mock -CommandName Invoke-RestMethod -MockWith { + Remove-Variable -Name TooManyRequestsFlag -Scope Global + return @{ success = $true } + } -ParameterFilter { + $Global:TooManyRequestsFlag -eq $true + } + + Mock -CommandName Start-Sleep -Verifiable + + $parameters = $defaultParameters.Clone() + $parameters.RetryAttempts = 2 + + $result = Invoke-AzDevOpsApiRestMethod @parameters + + $result | Should -BeOfType [System.Collections.Hashtable] + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $Message -like '*Too Many Requests*' + } + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $Message -like '*seconds before retrying*' + } + Assert-MockCalled -CommandName Start-Sleep -Times 1 + + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-AzDevOpsACLToken.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-AzDevOpsACLToken.tests.ps1 new file mode 100644 index 000000000..abe41de14 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-AzDevOpsACLToken.tests.ps1 @@ -0,0 +1,55 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Unit Tests for New-AzDevOpsACLToken function +Describe 'New-AzDevOpsACLToken' -Skip { + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath "New-AzDevOpsACLToken.tests.ps1" + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + ForEach ($file in $files) { + . $file.FullName + } + + } + + Context 'When TeamId is provided' { + It 'Should create a team-level access token' { + $OrganizationName = "Contoso" + $ProjectId = "MyProject" + $TeamId = "MyTeam" + $expectedToken = "vstfs:///Classification/TeamProject/$ProjectId/$TeamId" + + $result = New-AzDevOpsACLToken -OrganizationName $OrganizationName -ProjectId $ProjectId -TeamId $TeamId + + $result | Should -Be $expectedToken + } + } + + Context 'When TeamId is not provided' { + It 'Should create a project-level access token' { + $OrganizationName = "Contoso" + $ProjectId = "MyProject" + $expectedToken = "vstfs:///Classification/TeamProject/$ProjectId" + + $result = New-AzDevOpsACLToken -OrganizationName $OrganizationName -ProjectId $ProjectId + + $result | Should -Be $expectedToken + } + } + + Context 'When required parameters are missing' { + It 'Should throw an error if OrganizationName is missing' { + { New-AzDevOpsACLToken -ProjectId "MyProject" } | Should -Throw + } + + It 'Should throw an error if ProjectId is missing' { + { New-AzDevOpsACLToken -OrganizationName "Contoso" } | Should -Throw + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-InvalidOperationException.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-InvalidOperationException.tests.ps1 new file mode 100644 index 000000000..b10158b9c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/New-InvalidOperationException.tests.ps1 @@ -0,0 +1,53 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'New-InvalidOperationException' -Skip { + + BeforeAll { + + Mock -CommandName New-InvalidOperationException -MockWith { + param ( + [string]$Message, + [switch]$Throw + ) + + if (-not $Message) { + throw [System.Management.Automation.ParameterBindingValidationException]::new("Message parameter cannot be null or empty") + } + + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($Message), + "InvalidOperation", + [System.Management.Automation.ErrorCategory]::ConnectionError, + $null + ) + + if ($Throw) { + throw $errorRecord + } + + return $errorRecord + } + + } + + It 'Should return an ErrorRecord when given a valid message' { + $message = 'An error occurred' + $result = New-InvalidOperationException -Message $message + $result | Should -BeOfType [System.Management.Automation.ErrorRecord] + $result.Exception.Message | Should -BeExactly $message + $result.CategoryInfo.Category | Should -Be [System.Management.Automation.ErrorCategory]::ConnectionError + } + + It 'Should throw an ErrorRecord when -Throw is specified' { + $message = 'An error occurred' + { New-InvalidOperationException -Message $message -Throw } | Should -Throw -ExceptionType [System.Management.Automation.ErrorRecord] + } + + It 'Should fail if Message parameter is null' { + { New-InvalidOperationException -Message $null } | Should -Throw -ExceptionType [System.Management.Automation.ParameterBindingValidationException] + } + + It 'Should fail if Message parameter is empty' { + { New-InvalidOperationException -Message '' } | Should -Throw -ExceptionType [System.Management.Automation.ParameterBindingValidationException] + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/PreCommandLookupAction.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/PreCommandLookupAction.tests.ps1 new file mode 100644 index 000000000..2b535f50f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/PreCommandLookupAction.tests.ps1 @@ -0,0 +1,56 @@ +# Not implemented +Describe "PreCommandLookupAction" -skip { + BeforeAll { + $global:ExecutionContext = [pscustomobject]@{ + InvokeCommand = [pscustomobject]@{ + PreCommandLookupAction = $null + } + } + } + + It "Should throw if Add-AuthenticationHTTPHeader is called outside of Invoke-AzDevOpsApiRestMethod" { + $customCommand = [pscustomobject]@{ Name = "SomeOtherFunction" } + $global:MyInvocation = [pscustomobject]@{ MyCommand = $customCommand } + + { + & $global:ExecutionContext.InvokeCommand.PreCommandLookupAction.Invoke("Add-AuthenticationHTTPHeader", $null) + } | Should -Throw "The function 'Add-AuthenticationHTTPHeader' can only be called inside of 'Invoke-AzDevOpsApiRestMethod' function." + } + + It "Should not throw if Add-AuthenticationHTTPHeader is called inside of Invoke-AzDevOpsApiRestMethod" { + $customCommand = [pscustomobject]@{ Name = "Invoke-AzDevOpsApiRestMethod" } + $global:MyInvocation = [pscustomobject]@{ MyCommand = $customCommand } + + { + & $global:ExecutionContext.InvokeCommand.PreCommandLookupAction.Invoke("Add-AuthenticationHTTPHeader", $null) + } | Should -Not -Throw + } + + It "Should throw if Export- command is used within Invoke-AzDevOpsApiRestMethod function" { + $customCommand = [pscustomobject]@{ Name = "Invoke-AzDevOpsApiRestMethod" } + $global:MyInvocation = [pscustomobject]@{ MyCommand = $customCommand } + + { + & $global:ExecutionContext.InvokeCommand.PreCommandLookupAction.Invoke("Export-SomeData", $null) + } | Should -Throw "The command 'Export-SomeData' is not allowed to be used within 'Invoke-AzDevOpsApiRestMethod' function." + } + + It "Should throw if System.Runtime.InteropServices.Marshal is used outside of AuthenticationToken class" { + $customCommand = [pscustomobject]@{ Name = "SomeOtherClass" } + $global:MyInvocation = [pscustomobject]@{ MyCommand = $customCommand } + + { + & $global:ExecutionContext.InvokeCommand.PreCommandLookupAction.Invoke("System.Runtime.InteropServices.Marshal", $null) + } | Should -Throw "The command 'System.Runtime.InteropServices.Marshal' is not allowed to be used outside of 'AuthenticationToken' class." + } + + It "Should not throw if System.Runtime.InteropServices.Marshal is used inside AuthenticationToken class" { + $customCommand = [pscustomobject]@{ Name = "AuthenticationToken" } + $global:MyInvocation = [pscustomobject]@{ MyCommand = $customCommand } + + { + & $global:ExecutionContext.InvokeCommand.PreCommandLookupAction.Invoke("System.Runtime.InteropServices.Marshal", $null) + } | Should -Not -Throw + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/System/New-Thread.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/System/New-Thread.tests.ps1 new file mode 100644 index 000000000..076c47ee1 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/System/New-Thread.tests.ps1 @@ -0,0 +1,46 @@ +Describe 'New-Thread' -skip { + BeforeAll { + function New-Thread { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ScriptBlock]$ScriptBlock + ) + + Write-Verbose "[New-Thread] Started." + $thread = [System.Threading.Thread]::new($ScriptBlock) + $thread.Start() + Write-Verbose "[New-Thread] Completed." + return $thread + } + } + + It 'Throws an error when ScriptBlock is not provided' { + { New-Thread } | Should -Throw + } + + It 'Creates and starts a new thread' { + $scriptBlock = { Start-Sleep -Seconds 3 } + $thread = New-Thread -ScriptBlock $scriptBlock + $thread | Should -BeOfType 'System.Threading.Thread' + $thread.IsAlive | Should -Be $true + } + + It 'Thread runs the provided ScriptBlock' { + $hasRun = $false + $scriptBlock = { $script:hasRun = $true } + $thread = New-Thread -ScriptBlock $scriptBlock + $thread.Join() + $hasRun | Should -Be $true + } + + It 'Verbose output is produced' { + $verboseOutput = { + $scriptBlock = { Start-Sleep -Seconds 1 } + New-Thread -ScriptBlock $scriptBlock -Verbose + } | Out-String + $verboseOutput | Should -Contain '[New-Thread] Started.' + $verboseOutput | Should -Contain '[New-Thread] Completed.' + } +} + diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Invoke-AzDevOpsApiRestMethod.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Invoke-AzDevOpsApiRestMethod.Tests.ps1 deleted file mode 100644 index 5ffa3bbc0..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Invoke-AzDevOpsApiRestMethod.Tests.ps1 +++ /dev/null @@ -1,196 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - # Get default, parameter values - $defaultHttpContentType = 'application/json' - $defaultHttpBody = '' - $defaultRetryAttempts = 5 - $defaultRetryIntervalMs = 250 - - # Mock functions called in function - Mock Invoke-RestMethod {} - # Mock New-InvalidOperationException {} # Do not mock - Mock Start-Sleep {} - - # Generate valid, test cases - $testCasesValidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Valid' - $testCasesValidHttpMethods = Get-TestCase -ScopeName 'HttpMethod' -TestCaseName 'Valid' - $testCasesValidHttpRequestHeaders = Get-TestCase -ScopeName 'HttpRequestHeader' -TestCaseName 'Valid' - $testCasesValidApiUriHttpMethodHttpRequestHeaders = Join-TestCaseArray -TestCaseArray @( - $testCasesValidApiUris, - $testCasesValidHttpMethods, - $testCasesValidHttpRequestHeaders) -Expand - $testCasesValidApiUriHttpMethodHttpRequestHeaders3 = $testCasesValidApiUriHttpMethodHttpRequestHeaders | Select-Object -First 3 - - # Generate invalid, test cases - $testCasesInvalidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Invalid' - $testCasesInvalidHttpMethods = Get-TestCase -ScopeName 'HttpMethod' -TestCaseName 'Invalid' - $testCasesInvalidHttpRequestHeaders = Get-TestCase -ScopeName 'HttpRequestHeader' -TestCaseName 'Invalid' - $testCasesInvalidApiUriHttpMethodHttpRequestHeaders = Join-TestCaseArray -TestCaseArray @( - $testCasesInvalidApiUris, - $testCasesInvalidHttpMethods, - $testCasesInvalidHttpRequestHeaders) -Expand - $testCasesInvalidApiUriHttpMethodHttpRequestHeaders3 = $testCasesInvalidApiUriHttpMethodHttpRequestHeaders | Select-Object -First 3 - - - Context 'When input parameters are valid' { - - Context 'When called just with mandatory, "ApiUri", "HttpMethod" and "HttpRequestHeader" parameters' { - - It 'Should not throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $HttpMethod -HttpRequestHeader $HttpRequestHeader } | Should -Not -Throw - } - - It 'Should output nothing/null - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - $output = Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $HttpMethod -HttpRequestHeader $HttpRequestHeader - - $output | Should -BeNullOrEmpty - } - - It 'Should invoke "Invoke-RestMethod" exactly once - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - Mock Invoke-RestMethod {} -Verifiable - - Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $HttpMethod -HttpRequestHeader $HttpRequestHeader - - Assert-MockCalled Invoke-RestMethod -Times 1 -Exactly -Scope 'It' - } - - Context 'When "Invoke-RestMethod" throws an exception on every retry' { - Mock Invoke-RestMethod { throw "Some exception" } - - It 'Should invoke "Invoke-RestMethod" number of times equal to "RetryAttempts" parameter value + 1 - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - Mock Invoke-RestMethod { throw "Some exception" } -Verifiable - Mock New-InvalidOperationException {} - - Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $HttpMethod -HttpRequestHeader $HttpRequestHeader - - Assert-MockCalled Invoke-RestMethod -Times $($defaultRetryAttempts+1) -Exactly -Scope 'It' - } - - - It 'Should invoke "Start-Sleep" number of times equal to "RetryAttempts" parameter value + 1 - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - Mock Start-Sleep { } -Verifiable - - Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $HttpMethod -HttpRequestHeader $HttpRequestHeader - - Assert-MockCalled Start-Sleep -Times $($defaultRetryAttempts+1) -Exactly -Scope 'It' - } - - - It 'Should invoke "New-InvalidOperationException" exactly once - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - Mock New-InvalidOperationException {} -Verifiable - - Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $HttpMethod -HttpRequestHeader $HttpRequestHeader - - Assert-MockCalled New-InvalidOperationException -Times 1 -Exactly -Scope 'It' - } - - } - - } - } - - - Context 'When input parameters are invalid' { - - Context 'When called without mandatory, "ApiUri" parameter' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $null -HttpMethod $HttpMethod -HttpRequestHeader $HttpRequestHeader } | Should -Throw - } - - } - - Context 'When called without mandatory, "HttpMethod" parameter' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $null -HttpRequestHeader $HttpRequestHeader } | Should -Throw - } - - } - - Context 'When called without mandatory, "ApiUri" and "HttpMethod" parameters' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $null -HttpMethod $null -HttpRequestHeader $HttpRequestHeader } | Should -Throw - } - - } - - Context 'When called without mandatory, "HttpRequestHeader" parameter' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $HttpMethod -HttpRequestHeader $null } | Should -Throw - } - - } - - Context 'When called without mandatory, "ApiUri" and "HttpRequestHeader" parameters' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $null -HttpMethod $HttpMethod -HttpRequestHeader $null } | Should -Throw - } - - } - - Context 'When called without mandatory, "HttpMethod" and "HttpRequestHeader" parameters' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $ApiUri -HttpMethod $null -HttpRequestHeader $null } | Should -Throw - } - - } - - Context 'When called without mandatory, "ApiUri", "HttpMethod" and "HttpRequestHeader" parameters' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriHttpMethodHttpRequestHeaders3 { - param ([System.String]$ApiUri, [System.String]$HttpMethod, [Hashtable]$HttpRequestHeader) - - { Invoke-AzDevOpsApiRestMethod -ApiUri $null -HttpMethod $null -HttpRequestHeader $null } | Should -Throw - } - - } - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiHttpRequestHeader.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiHttpRequestHeader.Tests.ps1 deleted file mode 100644 index deb54d585..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiHttpRequestHeader.Tests.ps1 +++ /dev/null @@ -1,107 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidHttpRequestHeaders = Get-TestCase -ScopeName 'HttpRequestHeader' -TestCaseName 'Valid' - $testCasesInvalidHttpRequestHeaders = Get-TestCase -ScopeName 'HttpRequestHeader' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with "HttpRequestHeader" parameter value and the "IsValid" switch' { - - - Context 'When "HttpRequestHeader" parameter value is a valid "HttpRequestHeader"' { - - It 'Should not throw - ""' -TestCases $testCasesValidHttpRequestHeaders { - param ([Hashtable]$HttpRequestHeader) - - { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $HttpRequestHeader -IsValid } | Should -Not -Throw - } - - It 'Should return $true - ""' -TestCases $testCasesValidHttpRequestHeaders { - param ([Hashtable]$HttpRequestHeader) - - Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $HttpRequestHeader -IsValid | Should -BeTrue - } - } - - - Context 'When "HttpRequestHeader" parameter value is an invalid "HttpRequestHeader"' { - - It 'Should not throw - ""' -TestCases $testCasesInvalidHttpRequestHeaders { - param ([Hashtable]$HttpRequestHeader) - - { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $HttpRequestHeader -IsValid } | Should -Not -Throw - } - - It 'Should return $false - ""' -TestCases $testCasesInvalidHttpRequestHeaders { - param ([Hashtable]$HttpRequestHeader) - - Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $HttpRequestHeader -IsValid | Should -BeFalse - } - } - } - } - - - Context "When input parameters are invalid" { - - - Context 'When called with no/null parameter values/switches' { - - It 'Should throw' { - - { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader:$null -IsValid:$false } | Should -Throw - } - } - - - Context 'When "HttpRequestHeader" parameter value is a valid "HttpRequestHeader"' { - - - Context 'When called with "HttpRequestHeader" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesValidHttpRequestHeaders { - param ([Hashtable]$HttpRequestHeader) - - { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $HttpRequestHeader -IsValid:$false } | Should -Throw - } - } - } - - - Context 'When "HttpRequestHeader" parameter value is an invalid "HttpRequestHeader"' { - - - Context 'When called with "HttpRequestHeader" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidHttpRequestHeaders { - param ([Hashtable]$HttpRequestHeader) - - { Test-AzDevOpsApiHttpRequestHeader -HttpRequestHeader $HttpRequestHeader -IsValid:$false } | Should -Throw - } - } - } - - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResource.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResource.Tests.ps1 deleted file mode 100644 index 161d2add0..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResource.Tests.ps1 +++ /dev/null @@ -1,165 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - - # Mock functions called in function - Mock Get-AzDevOpsApiResource {} - - # Generate valid, test cases - $testCasesValidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Valid' - $testCasesValidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Valid' - $testCasesValidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Valid' - $testCasesValidApiUriPatResourceNames = Join-TestCaseArray -TestCaseArray @( - $testCasesValidApiUris, - $testCasesValidPats, - $testCasesValidResourceNames) -Expand - $testCasesValidApiUriPatResourceNames3 = $testCasesValidApiUriPatResourceNames | Select-Object -First 3 - - $validApiVersion = Get-TestCaseValue -ScopeName 'ApiVersion' -TestCaseName 'Valid' -First 1 - $validResourceId = Get-TestCaseValue -ScopeName 'ResourceId' -TestCaseName 'Valid' -First 1 - - # Generate invalid, test cases - $testCasesInvalidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Invalid' - $testCasesInvalidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Invalid' - $testCasesInvalidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Invalid' - $testCasesInvalidApiUriPatResourceNames = Join-TestCaseArray -TestCaseArray @( - $testCasesInvalidApiUris, - $testCasesInvalidPats, - $testCasesInvalidResourceNames) -Expand - $testCasesInvalidApiUriPatResourceNames3 = $testCasesInvalidApiUriPatResourceNames | Select-Object -First 3 - - $invalidApiVersion = Get-TestCaseValue -ScopeName 'ApiVersion' -TestCaseName 'Invalid' -First 1 - $invalidResourceId = Get-TestCaseValue -ScopeName 'ResourceId' -TestCaseName 'Invalid' -First 1 - - - Context 'When input parameters are valid' { - - - Context 'When called with mandatory, "ApiUri", "Pat", "ResourceName" and "ResourceId" parameters' { - - Context 'When the "ResourceId" parameter value is invalid' { - - It 'Should throw - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $invalidResourceId } | Should -Throw - } - } - - Context 'When the "ResourceId" parameter value is valid' { - - It 'Should not throw - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $validResourceId } | Should -Not -Throw - } - - It 'Should return a type of "boolean" - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - $output = Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $validResourceId - - $output | Should -BeOfType [boolean] - } - - It 'Should invoke "Get-AzDevOpsApiResource" only once - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - Mock Get-AzDevOpsApiResource {} -Verifiable - - Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $validResourceId | Out-Null - - Assert-MockCalled 'Get-AzDevOpsApiResource' -Times 1 -Exactly -Scope 'It' - } - - - Context 'When the resource exists' { - - It 'Should return $true - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - Mock Get-AzDevOpsApiResource { - return [System.Management.Automation.PSObject[]]@( - [System.Management.Automation.PSObject]@{ - id = '9a7ee4cf-7fa7-40e1-a3c0-1d0aacdaad92' - }, - [System.Management.Automation.PSObject]@{ - id = 'db79312c-8231-48b7-9967-db1bad53c881' - } - ) - } - - Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $validResourceId | Should -BeTrue - } - - } - - - Context 'When the resource does not exist' { - - It 'Should return $false - "", "", ""' -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - Mock Get-AzDevOpsApiResource { - return [System.Management.Automation.PSObject[]]@() - } - - Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName -ResourceId $validResourceId | Should -BeFalse - } - - } - - } - - Context "When also called with valid 'ApiVersion' parameter value" { - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Test-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $validApiVersion -Pat $Pat -ResourceName $ResourceName -ResourceId $validResourceId } | Should -Not -Throw - } - } - - Context "When also called with invalid 'ApiVersion' parameter value" { - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceNames3 { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Test-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $invalidApiVersion -Pat $Pat -ResourceName $ResourceName -ResourceId $validResourceId } | Should -Throw - } - } - } - } - - - Context 'When input parameters are invalid' { - - Context 'When called with mandatory, "ApiUri", "Pat" and "ResourceName" parameters' { - - It 'Should throw - "", "", ""' -TestCases $testCasesinvalidApiUriPatResourceNames { - param ([System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceName) - - { Test-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName $ResourceName } | Should -Throw - } - } - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceId.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceId.Tests.ps1 deleted file mode 100644 index 0951e05da..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceId.Tests.ps1 +++ /dev/null @@ -1,107 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidResourceIds = Get-TestCase -ScopeName 'ResourceId' -TestCaseName 'Valid' - $testCasesInvalidResourceIds = Get-TestCase -ScopeName 'ResourceId' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with "ResourceId" parameter value and the "IsValid" switch' { - - - Context 'When "ResourceId" parameter value is a valid "ResourceId"' { - - It 'Should not throw - ""' -TestCases $testCasesValidResourceIds { - param ([System.String]$ResourceId) - - { Test-AzDevOpsApiResourceId -ResourceId $ResourceId -IsValid } | Should -Not -Throw - } - - It 'Should return $true - ""' -TestCases $testCasesValidResourceIds { - param ([System.String]$ResourceId) - - Test-AzDevOpsApiResourceId -ResourceId $ResourceId -IsValid | Should -BeTrue - } - } - - - Context 'When "ResourceId" parameter value is an invalid "ResourceId"' { - - It 'Should not throw - ""' -TestCases $testCasesInvalidResourceIds { - param ([System.String]$ResourceId) - - { Test-AzDevOpsApiResourceId -ResourceId $ResourceId -IsValid } | Should -Not -Throw - } - - It 'Should return $false - ""' -TestCases $testCasesInvalidResourceIds { - param ([System.String]$ResourceId) - - Test-AzDevOpsApiResourceId -ResourceId $ResourceId -IsValid | Should -BeFalse - } - } - } - } - - - Context "When input parameters are invalid" { - - - Context 'When called with no/null parameter values/switches' { - - It 'Should throw' { - - { Test-AzDevOpsApiResourceId -ResourceId:$null -IsValid:$false } | Should -Throw - } - } - - - Context 'When "ResourceId" parameter value is a valid "ResourceId"' { - - - Context 'When called with "ResourceId" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesValidResourceIds { - param ([System.String]$ResourceId) - - { Test-AzDevOpsApiResourceId -ResourceId $ResourceId -IsValid:$false } | Should -Throw - } - } - } - - - Context 'When "ResourceId" parameter value is an invalid "ResourceId"' { - - - Context 'When called with "ResourceId" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidResourceIds { - param ([System.String]$ResourceId) - - { Test-AzDevOpsApiResourceId -ResourceId $ResourceId -IsValid:$false } | Should -Throw - } - } - } - - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceName.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceName.Tests.ps1 deleted file mode 100644 index 873bb161c..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiResourceName.Tests.ps1 +++ /dev/null @@ -1,107 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Valid' - $testCasesInvalidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with "ResourceName" parameter value and the "IsValid" switch' { - - - Context 'When "ResourceName" parameter value is a valid "ResourceName"' { - - It 'Should not throw - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - { Test-AzDevOpsApiResourceName -ResourceName $ResourceName -IsValid } | Should -Not -Throw - } - - It 'Should return $true - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - Test-AzDevOpsApiResourceName -ResourceName $ResourceName -IsValid | Should -BeTrue - } - } - - - Context 'When "ResourceName" parameter value is an invalid "ResourceName"' { - - It 'Should not throw - ""' -TestCases $testCasesInvalidResourceNames { - param ([System.String]$ResourceName) - - { Test-AzDevOpsApiResourceName -ResourceName $ResourceName -IsValid } | Should -Not -Throw - } - - It 'Should return $false - ""' -TestCases $testCasesInvalidResourceNames { - param ([System.String]$ResourceName) - - Test-AzDevOpsApiResourceName -ResourceName $ResourceName -IsValid | Should -BeFalse - } - } - } - } - - - Context "When input parameters are invalid" { - - - Context 'When called with no/null parameter values/switches' { - - It 'Should throw' { - - { Test-AzDevOpsApiResourceName -ResourceName:$null -IsValid:$false } | Should -Throw - } - } - - - Context 'When "ResourceName" parameter value is a valid "ResourceName"' { - - - Context 'When called with "ResourceName" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesValidResourceNames { - param ([System.String]$ResourceName) - - { Test-AzDevOpsApiResourceName -ResourceName $ResourceName -IsValid:$false } | Should -Throw - } - } - } - - - Context 'When "ResourceName" parameter value is an invalid "ResourceName"' { - - - Context 'When called with "ResourceName" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidResourceNames { - param ([System.String]$ResourceName) - - { Test-AzDevOpsApiResourceName -ResourceName $ResourceName -IsValid:$false } | Should -Throw - } - } - } - - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiTimeoutExceeded.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiTimeoutExceeded.Tests.ps1 deleted file mode 100644 index c23ef1c10..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiTimeoutExceeded.Tests.ps1 +++ /dev/null @@ -1,202 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesTimeoutExceeded = @( - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 1) - EndTime = [DateTime]::new(2020,11,12, 09,35,00, 252) # 1ms longer than timeout - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2020,11,12, 09,35,01, 0) # 1s longer than timeout - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2020,11,12, 09,35,01, 0) # 1s longer than timeout - TimeoutMs = 999 # Almost 1 second - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 1) - EndTime = [DateTime]::new(2020,11,12, 09,36,00, 1) # 1m longer than timeout - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 999) - EndTime = [DateTime]::new(2020,11,12, 10,35,00, 999) # 1h longer than timeout - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2020,11,13, 09,35,00, 0) # 1 day longer than timeout - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2020,12,12, 09,35,00, 0) # 1 month longer than timeout - TimeoutMs = 500 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2021,11,12, 09,35,00, 0) # 1 year longer than timeout - TimeoutMs = 300000 - } - ) - $testCasesTimeoutNotExceeded = @( - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2020,11,12, 09,35,00, 0) # Identical to StartTime - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 1) - EndTime = [DateTime]::new(2020,11,12, 09,35,00, 1) # Identical to StartTime - TimeoutMs = 500 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 1) - EndTime = [DateTime]::new(2020,11,12, 09,35,00, 250) # 1ms shorter than timeout (compared to StartTime) - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2020,11,12, 09,35,00, 249) # 1ms shorter than timeout (compared to StartTime) - TimeoutMs = 250 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - EndTime = [DateTime]::new(2020,11,12, 09,35,01, 0) # 1s longer than timeout - TimeoutMs = 1000 # 1 second - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 502) - EndTime = [DateTime]::new(2020,11,12, 09,35,00, 1) # EndTime 501ms before StartTime (negative timespan) - TimeoutMs = 550 - }, - @{ - StartTime = [DateTime]::new(2020,11,12, 09,35,00, 501) - EndTime = [DateTime]::new(2020,11,12, 09,35,00, 0) # EndTime 501ms before StartTime (negative timespan) - TimeoutMs = 500 - } - ) - - - Context 'When input parameters are valid' { - - - Context 'When called with mandatory "StartTime", "EndTime" and "TimeoutMs" parameter values' { - - Context 'When called with values that should generate an exceeded timeout' { - - It 'Should not throw - "","",""' -TestCases $testCasesTimeoutExceeded { - param ([Datetime]$StartTime, [Datetime]$EndTime, [Int32]$TimeoutMs) - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs } | Should -Not -Throw - } - - It 'Should return $true - "","",""' -TestCases $testCasesTimeoutExceeded { - param ([Datetime]$StartTime, [Datetime]$EndTime, [Int32]$TimeoutMs) - - Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs | Should -BeTrue - } - } - - Context 'When called with values that should not generate an exceeded timeout' { - - It 'Should not throw - "","",""' -TestCases $testCasesTimeoutNotExceeded { - param ([Datetime]$StartTime, [Datetime]$EndTime, [Int32]$TimeoutMs) - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs } | Should -Not -Throw - } - - It 'Should return $false - "","",""' -TestCases $testCasesTimeoutNotExceeded { - param ([Datetime]$StartTime, [Datetime]$EndTime, [Int32]$TimeoutMs) - - Test-AzDevOpsApiTimeoutExceeded -StartTime $StartTime -EndTime $EndTime -TimeoutMs $TimeoutMs | Should -BeFalse - } - } - } - } - - - Context "When input parameters are invalid" { - - [DateTime]$testTime = [DateTime]::new(2020,11,12, 09,35,00, 0) - [Int32]$testTimeoutMs = 250 - - Context 'When called with no/null parameter values' { - - It 'Should throw' { - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $null -EndTime $null -TimeoutMs $null } | Should -Throw - } - } - - Context 'When called with no/null "StartTime" parameter value' { - - It 'Should throw' { - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $null -EndTime $testTime -TimeoutMs $testTimeoutMs } | Should -Throw - } - } - - Context 'When called with no/null "EndTime" parameter value' { - - It 'Should throw' { - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $testTime -EndTime $null -TimeoutMs $testTimeoutMs } | Should -Throw - } - } - - Context 'When called with no/null "StartTime" and "EndTime" parameter values' { - - It 'Should throw' { - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $null -EndTime $null -TimeoutMs $testTimeoutMs } | Should -Throw - } - } - - Context 'When called with no/null "TimeoutMs" parameter value' { - - It 'Should throw' { - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $testTime -EndTime $testTime -TimeoutMs $null } | Should -Throw - } - } - - Context 'When called with no/null "StartTime" and "TimeoutMs" parameter values' { - - It 'Should throw' { - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $null -EndTime $testTime -TimeoutMs $null } | Should -Throw - } - } - - Context 'When called with no/null "EndTime" and "TimeoutMs" parameter values' { - - It 'Should throw' { - - { Test-AzDevOpsApiTimeoutExceeded -StartTime $testTime -EndTime $null -TimeoutMs $null } | Should -Throw - } - } - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiUri.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiUri.Tests.ps1 deleted file mode 100644 index 92cc4d3d8..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiUri.Tests.ps1 +++ /dev/null @@ -1,107 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Valid' - $testCasesInvalidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with "ApiUri" parameter value and the "IsValid" switch' { - - - Context 'When "ApiUri" parameter value is a valid "ApiUri"' { - - It 'Should not throw - ""' -TestCases $testCasesValidApiUris { - param ([System.String]$ApiUri) - - { Test-AzDevOpsApiUri -ApiUri $ApiUri -IsValid } | Should -Not -Throw - } - - It 'Should return $true - ""' -TestCases $testCasesValidApiUris { - param ([System.String]$ApiUri) - - Test-AzDevOpsApiUri -ApiUri $ApiUri -IsValid | Should -BeTrue - } - } - - - Context 'When "ApiUri" parameter value is an invalid "ApiUri"' { - - It 'Should not throw - ""' -TestCases $testCasesInvalidApiUris { - param ([System.String]$ApiUri) - - { Test-AzDevOpsApiUri -ApiUri $ApiUri -IsValid } | Should -Not -Throw - } - - It 'Should return $false - ""' -TestCases $testCasesInvalidApiUris { - param ([System.String]$ApiUri) - - Test-AzDevOpsApiUri -ApiUri $ApiUri -IsValid | Should -BeFalse - } - } - } - } - - - Context "When input parameters are invalid" { - - - Context 'When called with no/null parameter values/switches' { - - It 'Should throw' { - - { Test-AzDevOpsApiUri -ApiUri:$null -IsValid:$false } | Should -Throw - } - } - - - Context 'When "ApiUri" parameter value is a valid "ApiUri"' { - - - Context 'When called with "ApiUri" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesValidApiUris { - param ([System.String]$ApiUri) - - { Test-AzDevOpsApiUri -ApiUri $ApiUri -IsValid:$false } | Should -Throw - } - } - } - - - Context 'When "ApiUri" parameter value is an invalid "ApiUri"' { - - - Context 'When called with "ApiUri" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidApiUris { - param ([System.String]$ApiUri) - - { Test-AzDevOpsApiUri -ApiUri $ApiUri -IsValid:$false } | Should -Throw - } - } - } - - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiVersion.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiVersion.Tests.ps1 deleted file mode 100644 index 9e81ef7ab..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Test-AzDevOpsApiVersion.Tests.ps1 +++ /dev/null @@ -1,107 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidApiVersions = Get-TestCase -ScopeName 'ApiVersion' -TestCaseName 'Valid' - $testCasesInvalidApiVersions = Get-TestCase -ScopeName 'ApiVersion' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with "ApiVersion" parameter value and the "IsValid" switch' { - - - Context 'When "ApiVersion" parameter value is a valid "ApiVersion"' { - - It 'Should not throw - ""' -TestCases $testCasesValidApiVersions { - param ([System.String]$ApiVersion) - - { Test-AzDevOpsApiVersion -ApiVersion $ApiVersion -IsValid } | Should -Not -Throw - } - - It 'Should return $true - ""' -TestCases $testCasesValidApiVersions { - param ([System.String]$ApiVersion) - - Test-AzDevOpsApiVersion -ApiVersion $ApiVersion -IsValid | Should -BeTrue - } - } - - - Context 'When "ApiVersion" parameter value is an invalid "ApiVersion"' { - - It 'Should not throw - ""' -TestCases $testCasesInvalidApiVersions { - param ([System.String]$ApiVersion) - - { Test-AzDevOpsApiVersion -ApiVersion $ApiVersion -IsValid } | Should -Not -Throw - } - - It 'Should return $false - ""' -TestCases $testCasesInvalidApiVersions { - param ([System.String]$ApiVersion) - - Test-AzDevOpsApiVersion -ApiVersion $ApiVersion -IsValid | Should -BeFalse - } - } - } - } - - - Context "When input parameters are invalid" { - - - Context 'When called with no/null parameter values/switches' { - - It 'Should throw' { - - { Test-AzDevOpsApiVersion -ApiVersion:$null -IsValid:$false } | Should -Throw - } - } - - - Context 'When "ApiVersion" parameter value is a valid "ApiVersion"' { - - - Context 'When called with "ApiVersion" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesValidApiVersions { - param ([System.String]$ApiVersion) - - { Test-AzDevOpsApiVersion -ApiVersion $ApiVersion -IsValid:$false } | Should -Throw - } - } - } - - - Context 'When "ApiVersion" parameter value is an invalid "ApiVersion"' { - - - Context 'When called with "ApiVersion" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidApiVersions { - param ([System.String]$ApiVersion) - - { Test-AzDevOpsApiVersion -ApiVersion $ApiVersion -IsValid:$false } | Should -Throw - } - } - } - - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Wait-AzDevOpsApiResource.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Wait-AzDevOpsApiResource.Tests.ps1 deleted file mode 100644 index c5be79a5f..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Wait-AzDevOpsApiResource.Tests.ps1 +++ /dev/null @@ -1,615 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Api\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - # Get default, parameter values - $defaultWaitIntervalMilliseconds = Get-AzDevOpsApiWaitIntervalMs - $defaultWaitTimeoutMilliseconds = Get-AzDevOpsApiWaitTimeoutMs - - # Mock functions called in function - Mock Get-AzDevOpsApiWaitIntervalMs {} - Mock Get-AzDevOpsApiWaitTimeoutMs {} - # Mock Get-Date {} # Do not mock - # Mock New-InvalidOperationException {} # Do not mock - Mock Start-Sleep {} - Mock Test-AzDevOpsApiResource {} - Mock Test-AzDevOpsApiTimeoutExceeded {} - - # Generate valid, test cases - $testCasesValidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Valid' - $testCasesValidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Valid' - $testCasesValidResourceIds = Get-TestCase -ScopeName 'ResourceId' -TestCaseName 'Valid' - $testCasesValidApiUriPatResourceIds = Join-TestCaseArray -TestCaseArray @( - $testCasesValidApiUris, - $testCasesValidPats, - $testCasesValidResourceIds) -Expand - $testCasesValidApiUriPatResourceIds3 = $testCasesValidApiUriPatResourceIds | Select-Object -First 3 - - $validApiVersion = Get-TestCaseValue -ScopeName 'ApiVersion' -TestCaseName 'Valid' -First 1 - - # Generate invalid, test cases - $testCasesInvalidApiUris = Get-TestCase -ScopeName 'ApiUri' -TestCaseName 'Invalid' - $testCasesInvalidPats = Get-TestCase -ScopeName 'Pat' -TestCaseName 'Invalid' - $testCasesInvalidResourceIds = Get-TestCase -ScopeName 'ResourceId' -TestCaseName 'Invalid' - $testCasesInvalidApiUriPatResourceIds = Join-TestCaseArray -TestCaseArray @( - $testCasesInvalidApiUris, - $testCasesInvalidPats, - $testCasesInvalidResourceIds) -Expand - $testCasesInvalidApiUriPatResourceIds3 = $testCasesInvalidApiUriPatResourceIds | Select-Object -First 3 - - $invalidApiVersion = Get-TestCaseValue -ScopeName 'ApiVersion' -TestCaseName 'Invalid' -First 1 - - - Context 'When input parameters are valid' { - - - Context "When called with all, mandatory parameters ('ApiUri', 'Pat' and 'ResourceId')" { - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceId $ResourceId } | Should -Throw - } - - - Context "When also called with mandatory, 'IsPresent', switch parameter" { - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } - Mock Test-AzDevOpsApiResource { return $true } - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent } | Should -Not -Throw - } - - It "Should output null/nothing - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $output = Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - $output | Should -BeNullOrEmpty - } - - It "Should invoke 'Get-AzDevOpsApiWaitIntervalMs' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Get-AzDevOpsApiWaitIntervalMs' -Times 1 -Exactly -Scope It - } - - It "Should invoke 'Get-AzDevOpsApiWaitTimeoutMs' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Get-AzDevOpsApiWaitTimeoutMs' -Times 1 -Exactly -Scope It - } - - Context "When also called with valid 'ApiVersion' parameter value" { - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $validApiVersion -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent } | Should -Not -Throw - } - } - - Context "When also called with invalid 'ApiVersion' parameter value" { - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $invalidApiVersion -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent } | Should -Throw - } - } - - Context "When 'Test-AzDevOpsApiResource' returns true" { - - It "Should invoke 'Test-AzDevOpsApiResource' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Test-AzDevOpsApiResource { return $true } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Test-AzDevOpsApiResource' -Times 1 -Exactly -Scope It - } - - It "Should invoke 'Get-Date' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-Date { return [DateTime]::get_UtcNow() } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Get-Date' -Times 1 -Exactly -Scope It - } - - It "Should not invoke 'Start-Sleep' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Start-Sleep {} -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Start-Sleep' -Times 0 -Exactly -Scope It - } - - It "Should not invoke 'Test-AzDevOpsApiTimeoutExceeded' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Test-AzDevOpsApiTimeoutExceeded {} -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Test-AzDevOpsApiTimeoutExceeded' -Times 0 -Exactly -Scope It - } - } - - - Context "When 'Test-AzDevOpsApiResource' returns false, then true" { - - - Context "When 'WaitTimeoutMilliseconds' has not been exceeded" { - Mock Get-AzDevOpsApiWaitTimeoutMs {250} # 250ms - Mock Test-AzDevOpsApiResource { - $script:mockTestAzDevOpsApiResourceInvoked = !($script:mockTestAzDevOpsApiResourceInvoked) - return !($script:mockTestAzDevOpsApiResourceInvoked) - } - Mock Test-AzDevOpsApiTimeoutExceeded { return $false } - - It "Should invoke 'Test-AzDevOpsApiResource' exactly twice - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $script:mockTestAzDevOpsApiResourceInvoked = $false - Mock Test-AzDevOpsApiResource { - $script:mockTestAzDevOpsApiResourceInvoked = !($script:mockTestAzDevOpsApiResourceInvoked) - return !($script:mockTestAzDevOpsApiResourceInvoked) - } - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Test-AzDevOpsApiResource' -Times 2 -Exactly -Scope It - } - - It "Should invoke 'Get-Date' exactly twice - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-Date { return [DateTime]::get_UtcNow() } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Get-Date' -Times 2 -Exactly -Scope It - } - - It "Should invoke 'Start-Sleep' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $script:mockTestAzDevOpsApiResourceInvoked = $false # for 'Test-AzDevOpsApiResource' mock - Mock Start-Sleep {} -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Start-Sleep' -Times 1 -Exactly -Scope It - } - - It "Should invoke 'Test-AzDevOpsApiTimeoutExceeded' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $script:mockTestAzDevOpsApiResourceInvoked = $false # for 'Test-AzDevOpsApiResource' mock - Mock Test-AzDevOpsApiTimeoutExceeded { return $false } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent - - Assert-MockCalled 'Test-AzDevOpsApiTimeoutExceeded' -Times 1 -Exactly -Scope It - } - - } - } - - - Context "When 'Test-AzDevOpsApiResource' returns false, and exceeds timeout (i.e. 'Test-AzDevOpsApiTimeoutExceeded' returns true)" { - Mock Get-AzDevOpsApiWaitTimeoutMs {250} # 250ms - Mock Test-AzDevOpsApiTimeoutExceeded { return $true } # i.e. Timeout exceeded - Mock Test-AzDevOpsApiResource { return $false } # i.e. ApiResource has not completed - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent } | Should -Throw - } - } - - - Context "When also called with optional, 'WaitIntervalMilliseconds' parameter" { - - $exampleWaitIntervalMilliseconds = $defaultWaitIntervalMilliseconds - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds } | Should -Not -Throw - } - - It "Should not invoke 'Get-AzDevOpsApiWaitIntervalMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitIntervalMs' -Times 0 -Exactly -Scope It - } - - - } - - - Context "When also called with optional, 'WaitTimeoutMilliseconds' parameter" { - - $exampleWaitTimeoutMilliseconds = $defaultWaitTimeoutMilliseconds - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds } | Should -Not -Throw - } - - It "Should not invoke 'Get-AzDevOpsApiWaitTimeoutMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitTimeoutMs' -Times 0 -Exactly -Scope It - } - - } - - - Context "When also called with both optional, 'WaitIntervalMilliseconds' and 'WaitTimeoutMilliseconds' parameters" { - - $exampleWaitIntervalMilliseconds = $defaultWaitIntervalMilliseconds - $exampleWaitTimeoutMilliseconds = $defaultWaitTimeoutMilliseconds - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent ` - -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds } | Should -Not -Throw - } - - It "Should not invoke 'Get-AzDevOpsApiWaitIntervalMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent ` - -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitIntervalMs' -Times 0 -Exactly -Scope It - } - - It "Should not invoke 'Get-AzDevOpsApiWaitTimeoutMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent ` - -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitTimeoutMs' -Times 0 -Exactly -Scope It - } - - } - - - } - - - Context "When also called with mandatory, 'IsAbsent', switch parameter" { - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } - Mock Test-AzDevOpsApiResource { return $false } - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent } | Should -Not -Throw - } - - It "Should output null/nothing - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $output = Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - $output | Should -BeNullOrEmpty - } - - It "Should invoke 'Get-AzDevOpsApiWaitIntervalMs' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Get-AzDevOpsApiWaitIntervalMs' -Times 1 -Exactly -Scope It - } - - It "Should invoke 'Get-AzDevOpsApiWaitTimeoutMs' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Get-AzDevOpsApiWaitTimeoutMs' -Times 1 -Exactly -Scope It - } - - - Context "When also called with valid 'ApiVersion' parameter value" { - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $validApiVersion -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent } | Should -Not -Throw - } - } - - - Context "When also called with invalid 'ApiVersion' parameter value" { - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -ApiVersion $invalidApiVersion -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent } | Should -Throw - } - } - - - Context "When 'Test-AzDevOpsApiResource' returns false" { - - It "Should invoke 'Test-AzDevOpsApiResource' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Test-AzDevOpsApiResource { return $false } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Test-AzDevOpsApiResource' -Times 1 -Exactly -Scope It - } - - It "Should invoke 'Get-Date' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-Date { return [DateTime]::get_UtcNow() } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Get-Date' -Times 1 -Exactly -Scope It - } - - It "Should not invoke 'Start-Sleep' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Start-Sleep {} -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Start-Sleep' -Times 0 -Exactly -Scope It - } - - It "Should not invoke 'Test-AzDevOpsApiTimeoutExceeded' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Test-AzDevOpsApiTimeoutExceeded {} -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Test-AzDevOpsApiTimeoutExceeded' -Times 0 -Exactly -Scope It - } - } - - - Context "When 'Test-AzDevOpsApiResource' returns true, then false" { - - - Context "When 'WaitTimeoutMilliseconds' has not been exceeded" { - Mock Get-AzDevOpsApiWaitTimeoutMs {250} # 250ms - Mock Test-AzDevOpsApiResource { - $script:mockTestAzDevOpsApiResourceInvoked = !($script:mockTestAzDevOpsApiResourceInvoked) - return $script:mockTestAzDevOpsApiResourceInvoked - } - Mock Test-AzDevOpsApiTimeoutExceeded { return $false } - - It "Should invoke 'Test-AzDevOpsApiResource' exactly twice - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $script:mockTestAzDevOpsApiResourceInvoked = $false - Mock Test-AzDevOpsApiResource { - $script:mockTestAzDevOpsApiResourceInvoked = !($script:mockTestAzDevOpsApiResourceInvoked) - return $script:mockTestAzDevOpsApiResourceInvoked - } - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Test-AzDevOpsApiResource' -Times 2 -Exactly -Scope It - } - - It "Should invoke 'Get-Date' exactly twice - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-Date { return [DateTime]::get_UtcNow() } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Get-Date' -Times 2 -Exactly -Scope It - } - - It "Should invoke 'Start-Sleep' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $script:mockTestAzDevOpsApiResourceInvoked = $false # for 'Test-AzDevOpsApiResource' mock - Mock Start-Sleep {} -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Start-Sleep' -Times 1 -Exactly -Scope It - } - - It "Should invoke 'Test-AzDevOpsApiTimeoutExceeded' exactly once - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - $script:mockTestAzDevOpsApiResourceInvoked = $false # for 'Test-AzDevOpsApiResource' mock - Mock Test-AzDevOpsApiTimeoutExceeded { return $false } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent - - Assert-MockCalled 'Test-AzDevOpsApiTimeoutExceeded' -Times 1 -Exactly -Scope It - } - - } - } - - - Context "When 'Test-AzDevOpsApiResource' returns false, and exceeds timeout (i.e. 'Test-AzDevOpsApiTimeoutExceeded' returns true)" { - Mock Get-AzDevOpsApiWaitTimeoutMs {250} # 250ms - Mock Test-AzDevOpsApiTimeoutExceeded { return $true } # i.e. Timeout exceeded - Mock Test-AzDevOpsApiResource { return $true } # i.e. ApiResource has not completed - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent } | Should -Throw - } - } - - - Context "When also called with optional, 'WaitIntervalMilliseconds' parameter" { - - $exampleWaitIntervalMilliseconds = $defaultWaitIntervalMilliseconds - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds } | Should -Not -Throw - } - - It "Should not invoke 'Get-AzDevOpsApiWaitIntervalMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitIntervalMs' -Times 0 -Exactly -Scope It - } - - - } - - - Context "When also called with optional, 'WaitTimeoutMilliseconds' parameter" { - - $exampleWaitTimeoutMilliseconds = $defaultWaitTimeoutMilliseconds - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds } | Should -Not -Throw - } - - It "Should not invoke 'Get-AzDevOpsApiWaitTimeoutMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitTimeoutMs' -Times 0 -Exactly -Scope It - } - - } - - - Context "When also called with both optional, 'WaitIntervalMilliseconds' and 'WaitTimeoutMilliseconds' parameters" { - - $exampleWaitIntervalMilliseconds = $defaultWaitIntervalMilliseconds - $exampleWaitTimeoutMilliseconds = $defaultWaitTimeoutMilliseconds - - It "Should not throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent ` - -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds } | Should -Not -Throw - } - - It "Should not invoke 'Get-AzDevOpsApiWaitIntervalMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitIntervalMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent ` - -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitIntervalMs' -Times 0 -Exactly -Scope It - } - - It "Should not invoke 'Get-AzDevOpsApiWaitTimeoutMs' - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - Mock Get-AzDevOpsApiWaitTimeoutMs { return 250 } -Verifiable - - Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsAbsent ` - -WaitTimeoutMilliseconds $exampleWaitTimeoutMilliseconds -WaitIntervalMilliseconds $exampleWaitIntervalMilliseconds - - Assert-MockCalled 'Get-AzDevOpsApiWaitTimeoutMs' -Times 0 -Exactly -Scope It - } - - } - - } - - - Context "When also called with both mandatory, 'IsPresent' and 'IsAbsent', switch parameters" { - - It "Should throw - '', '', ''" -TestCases $testCasesValidApiUriPatResourceIds3 { - param ( [System.String]$ApiUri, [System.String]$Pat, [System.String]$ResourceId ) - - { Wait-AzDevOpsApiResource -ApiUri $ApiUri -Pat $Pat -ResourceName 'Project' -ResourceId $ResourceId -IsPresent -IsAbsent } | Should -Throw - } - } - } - } - - - Context "When input parameters are invalid" { - - # TODO - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.TestInitialization.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.TestInitialization.ps1 deleted file mode 100644 index aeea1dc63..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/AzureDevOpsDsc.Common.TestInitialization.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -<# - .SYNOPSIS - Automated unit test for classes in AzureDevOpsDsc. -#> - -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestHelper.psm1') -Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '\..\Modules\TestHelpers\CommonTestCases.psm1') - -$script:dscModuleName = 'AzureDevOpsDsc' -$script:dscModule = Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1 -$script:dscModuleFile = $($script:dscModule.ModuleBase +'\'+ $script:dscModuleName + ".psd1") -Get-Module -Name $script:dscModuleName -All | - Remove-Module $script:dscModuleName -Force -ErrorAction SilentlyContinue - -$script:subModuleName = 'AzureDevOpsDsc.Common' -Import-Module -Name $script:dscModuleFile -Force - -Get-Module -Name $script:subModuleName -All | - Remove-Module -Force -ErrorAction SilentlyContinue -$script:subModulesFolder = Join-Path -Path $script:dscModule.ModuleBase -ChildPath 'Modules' -$script:subModuleFile = Join-Path $script:subModulesFolder "$($script:subModuleName)/$($script:subModuleName).psd1" -Import-Module -Name $script:subModuleFile -Force #-Verbose diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Connection/Functions/Private/Test-AzDevOpsPatCredential.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Connection/Functions/Private/Test-AzDevOpsPatCredential.Tests.ps1 deleted file mode 100644 index 87b2a93f8..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc.Common/Connection/Functions/Private/Test-AzDevOpsPatCredential.Tests.ps1 +++ /dev/null @@ -1,107 +0,0 @@ - -# Initialize tests for module function -. $PSScriptRoot\..\..\..\..\AzureDevOpsDsc.Common.Tests.Initialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - $script:dscModuleName = 'AzureDevOpsDsc' - $script:moduleVersion = $(Get-Module -Name $script:dscModuleName -ListAvailable | Select-Object -First 1).Version - $script:subModuleName = 'AzureDevOpsDsc.Common' - $script:subModuleBase = $(Get-Module $script:subModuleName).ModuleBase - $script:commandName = $(Get-Item $PSCommandPath).BaseName.Replace('.Tests','') - $script:commandScriptPath = Join-Path "$PSScriptRoot\..\..\..\..\..\..\..\" -ChildPath "output\$($script:dscModuleName)\$($script:moduleVersion)\Modules\$($script:subModuleName)\Connection\Functions\Private\$($script:commandName).ps1" - $script:tag = @($($script:commandName -replace '-')) - - . $script:commandScriptPath - - - Describe "$script:subModuleName\Api\Function\$script:commandName" -Tag $script:tag { - - $testCasesValidPatCredentials = Get-TestCase -ScopeName 'PatCredential' -TestCaseName 'Valid' - $testCasesInvalidPatCredentials = Get-TestCase -ScopeName 'PatCredential' -TestCaseName 'Invalid' - - - Context 'When input parameters are valid' { - - - Context 'When called with "PatCredential" parameter value and the "IsValid" switch' { - - - Context 'When "PatCredential" parameter value is a valid "PatCredential"' { - - It 'Should not throw - ""' -TestCases $testCasesValidPatCredentials { - param ([PSCredential]$PatCredential) - - { Test-AzDevOpsPatCredential -PatCredential $PatCredential -IsValid } | Should -Not -Throw - } - - It 'Should return $true - ""' -TestCases $testCasesValidPatCredentials { - param ([PSCredential]$PatCredential) - - Test-AzDevOpsPatCredential -PatCredential $PatCredential -IsValid | Should -BeTrue - } - } - - - Context 'When "PatCredential" parameter value is an invalid "PatCredential"' { - - It 'Should not throw - ""' -TestCases $testCasesInvalidPatCredentials { - param ([PSCredential]$PatCredential) - - { Test-AzDevOpsPatCredential -PatCredential $PatCredential -IsValid } | Should -Not -Throw - } - - It 'Should return $false - ""' -TestCases $testCasesInvalidPatCredentials { - param ([PSCredential]$PatCredential) - - Test-AzDevOpsPatCredential -PatCredential $PatCredential -IsValid | Should -BeFalse - } - } - } - } - - - Context "When input parameters are invalid" { - - - Context 'When called with no/null/empty parameter values/switches' { - - It 'Should throw' { - - { Test-AzDevOpsPatCredential -PatCredential $([PSCredential]::Empty) -IsValid:$false } | Should -Throw - } - } - - - Context 'When "PatCredential" parameter value is a valid "PatCredential"' { - - - Context 'When called with "PatCredential" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesValidPatCredentials { - param ([PSCredential]$PatCredential) - - { Test-AzDevOpsPatCredential -PatCredential $PatCredential -IsValid:$false } | Should -Throw - } - } - } - - - Context 'When "PatCredential" parameter value is an invalid "PatCredential"' { - - - Context 'When called with "PatCredential" parameter value but a $false "IsValid" switch value' { - - It 'Should throw - ""' -TestCases $testCasesInvalidPatCredentials { - param ([PSCredential]$PatCredential) - - { Test-AzDevOpsPatCredential -PatCredential $PatCredential -IsValid:$false } | Should -Throw - } - } - } - - - } - } -} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/LocalizedData/000.LocalizedDataAzACLTokenPatten.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/LocalizedData/000.LocalizedDataAzACLTokenPatten.tests.ps1 new file mode 100644 index 000000000..b0a1bb017 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/LocalizedData/000.LocalizedDataAzACLTokenPatten.tests.ps1 @@ -0,0 +1,61 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Define tests +Describe "Testing LocalizedDataAzACLTokenPattern regex patterns" { + + BeforeAll { + $source = Get-FunctionItem '000.LocalizedDataAzACLTokenPatten.ps1' + + . $source.FullName + + } + + It "OrganizationGit should match 'repoV2'" { + 'repoV2' -match $LocalizedDataAzACLTokenPatten.OrganizationGit | Should -BeTrue + } + + It "GitProject should match 'repoV2/Project123'" { + 'repoV2/Project123' -match $LocalizedDataAzACLTokenPatten.GitProject | Should -BeTrue + } + + It "GitRepository should match 'repoV2/Project123/Repo456'" { + 'repoV2/Project123/Repo456' -match $LocalizedDataAzACLTokenPatten.GitRepository | Should -BeTrue + } + + It "GitBranch should match 'repoV2/Project123/Repo456/refs/heads/main'" { + 'repoV2/Project123/Repo456/refs/heads/main' -match $LocalizedDataAzACLTokenPatten.GitBranch | Should -BeTrue + } + + It "GroupPermission should match 'Project123\Group456'" { + 'Project123\Group456' -match $LocalizedDataAzACLTokenPatten.GroupPermission | Should -BeTrue + } + + It "ResourcePermission should match 'Project123'" { + 'Project123' -match $LocalizedDataAzACLTokenPatten.ResourcePermission | Should -BeTrue + } + + # Negative tests + It "OrganizationGit should not match 'repoV3'" { + 'repoV3' -match $LocalizedDataAzACLTokenPatten.OrganizationGit | Should -BeFalse + } + + It "GitProject should not match 'repoV2/'" { + 'repoV2/' -match $LocalizedDataAzACLTokenPatten.GitProject | Should -BeFalse + } + + It "GitRepository should not match 'repoV2/Project123/'" { + 'repoV2/Project123/' -match $LocalizedDataAzACLTokenPatten.GitRepository | Should -BeFalse + } + + It "GitBranch should not match 'repoV2/Project123/Repo456/branches/main'" { + 'repoV2/Project123/Repo456/branches/main' -match $LocalizedDataAzACLTokenPatten.GitBranch | Should -BeFalse + } + + It "GroupPermission should not match 'Project123'" { + 'Project123' -match $LocalizedDataAzACLTokenPatten.GroupPermission | Should -BeFalse + } + + It "ResourcePermission should not match 'Project123\Extra'" { + 'Project123\Extra' -match $LocalizedDataAzACLTokenPatten.ResourcePermission | Should -BeFalse + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOperationId.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOperationId.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOperationId.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOperationId.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsOrganizationName.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsPat.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectDescription.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectId.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectId.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectId.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectId.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Test-AzDevOpsProjectName.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Private/Wait-AzDevOpsOperation.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsOperation.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Get-AzDevOpsOperation.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsOperation.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Get-AzDevOpsOperation.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsProject.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Get-AzDevOpsProject.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Get-AzDevOpsProject.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Get-AzDevOpsProject.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDevOpsProject.Tests.Old.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/New-AzDevOpsProject.Tests.Old.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDevOpsProject.Tests.Old.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/New-AzDevOpsProject.Tests.Old.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Test-AzDevOpsOperation.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Test-AzDevOpsOperation.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Test-AzDevOpsOperation.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Test-AzDevOpsOperation.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Test-AzDevOpsProject.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Test-AzDevOpsProject.Tests.ps1.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Test-AzDevOpsProject.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/Archive/Test-AzDevOpsProject.Tests.ps1.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Get-AzDoGitPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Get-AzDoGitPermission.tests.ps1 new file mode 100644 index 000000000..5d3ad181c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Get-AzDoGitPermission.tests.ps1 @@ -0,0 +1,196 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-AzDoGitPermission Tests' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoGitPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + Function Mock-Get-CacheItem { + param ( + [string]$Key, + [string]$Type + ) + switch ($Type) { + 'LiveRepositories' { return @{ id = 123; Name = "TestRepository" } } + 'SecurityNamespaces' { return @{ namespaceId = "TestNamespaceId" } } + default { return $null } + } + } + + Function Mock-Get-DevOpsACL { + param ( + [Parameter(Mandatory = $true)] + [string]$OrganizationName, + [Parameter(Mandatory = $true)] + [string]$SecurityDescriptorId + ) + return @( @{ Token = @{ Type = 'GitRepository'; RepoId = 123 }; Permission = 'Allow' } ) + } + + Function Mock-ConvertTo-FormattedACL { + param ( + [Parameter(Mandatory = $true)] + $SecurityNamespace, + [Parameter(Mandatory = $true)] + $OrganizationName + ) + return @( @{ Token = @{ Type = 'GitRepository'; RepoId = 123 }; Permission = 'Allow' } ) + } + + Function Mock-ConvertTo-ACL { + param ( + [Parameter(Mandatory = $true)] + $Permissions, + [Parameter(Mandatory = $true)] + $SecurityNamespace, + [Parameter(Mandatory = $true)] + $isInherited, + [Parameter(Mandatory = $true)] + $OrganizationName, + [Parameter(Mandatory = $true)] + $TokenName + ) + return @( @{ Token = @{ Type = 'GitRepository'; RepoId = 123 }; Permission = 'Deny' } ) + } + + Function Mock-Test-ACLListforChanges { + param ( + [Parameter(Mandatory = $true)] + $ReferenceACLs, + [Parameter(Mandatory = $true)] + $DifferenceACLs + ) + return @{ + propertiesChanged = @('Permission'); + status = 'Changed'; + reason = 'Permission mismatch' + } + } + + Mock -CommandName Write-Verbose + Mock -CommandName Write-Warning + Mock -CommandName Get-CacheItem -MockWith { Mock-Get-CacheItem -Key $Key -Type $Type } + Mock -CommandName Get-DevOpsACL -MockWith { Mock-Get-DevOpsACL -OrganizationName $OrganizationName -SecurityDescriptorId $SecurityDescriptorId } + Mock -CommandName ConvertTo-FormattedACL -MockWith { Mock-ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName } + Mock -CommandName ConvertTo-ACL -MockWith { Mock-ConvertTo-ACL -Permissions $Permissions -SecurityNamespace $SecurityNamespace -isInherited $isInherited -OrganizationName $OrganizationName -TokenName $TokenName } + Mock -CommandName Test-ACLListforChanges -MockWith { Mock-Test-ACLListforChanges -ReferenceACLs $ReferenceACLs -DifferenceACLs $DifferenceACLs } + } + + It 'Should retrieve repository and namespace, and compare ACLs correctly' { + $ProjectName = 'TestProject' + $RepositoryName = 'TestRepository' + $isInherited = $true + $Permissions = @(@{ 'Permission' = 'Deny' }) + + $result = Get-AzDoGitPermission -ProjectName $ProjectName -RepositoryName $RepositoryName -isInherited $isInherited -Permissions $Permissions + + $result | Should -Not -BeNullOrEmpty + $result.status | Should -Be 'Changed' + $result.propertiesChanged | Should -Contain 'Permission' + } + + It "Should return 'Unchanged' if the permissions are the same" { + $ProjectName = 'TestProject' + $RepositoryName = 'TestRepository' + $isInherited = $true + $Permissions = @(@{ 'Permission' = 'Allow' }) + + Mock -CommandName Test-ACLListforChanges -MockWith { + return @{ + propertiesChanged = @() + status = 'Unchanged' + reason = 'No change' + } + } + + $result = Get-AzDoGitPermission -ProjectName $ProjectName -RepositoryName $RepositoryName -isInherited $isInherited -Permissions $Permissions + + $result | Should -Not -BeNullOrEmpty + $result.status | Should -Be 'Unchanged' + $result.propertiesChanged | Should -BeNullOrEmpty + } + + It "Should returned 'Changed' if one of the permissions is null" { + $ProjectName = 'TestProject' + $RepositoryName = 'TestRepository' + $isInherited = $true + $Permissions = @(@{ 'Permission' = $null }) + + $result = Get-AzDoGitPermission -ProjectName $ProjectName -RepositoryName $RepositoryName -isInherited $isInherited -Permissions $Permissions + + $result | Should -Not -BeNullOrEmpty + $result.status | Should -Be 'Changed' + $result.propertiesChanged | Should -Contain 'Permission' + } + + It "Should return 'NotFound' if the repository is not found" { + + Mock -CommandName Get-CacheItem -MockWith { return $null } -ParameterFilter { $Type -eq 'LiveRepositories' } + + $ProjectName = 'TestProject' + $RepositoryName = 'NotFoundRepository' + $isInherited = $true + $Permissions = @(@{ 'Permission' = 'Allow' }) + + $result = Get-AzDoGitPermission -ProjectName $ProjectName -RepositoryName $RepositoryName -isInherited $isInherited -Permissions $Permissions + + $result | Should -Not -BeNullOrEmpty + $result.status | Should -Be 'NotFound' + $result.propertiesChanged | Should -BeNullOrEmpty + } + + It "Should return 'NotFound' if Get-DevOpsACL is null" { + Mock -CommandName Get-DevOpsACL -MockWith { return $null } + Mock -CommandName Write-Warning -Verifiable + + $ProjectName = 'TestProject' + $RepositoryName = 'TestRepository' + $isInherited = $true + $Permissions = @(@{ 'Permission' = 'Allow' }) + + $result = Get-AzDoGitPermission -ProjectName $ProjectName -RepositoryName $RepositoryName -isInherited $isInherited -Permissions $Permissions + + $result | Should -Not -BeNullOrEmpty + $result.status | Should -Be 'NotFound' + Assert-VerifiableMock + } + + It "Should return 'NotFound' if ConvertTo-FormattedACL is null" { + Mock -CommandName ConvertTo-FormattedACL -MockWith { return $null } + Mock -CommandName Write-Warning -Verifiable + + $ProjectName = 'TestProject' + $RepositoryName = 'TestRepository' + $isInherited = $true + $Permissions = @(@{ 'Permission' = 'Allow' }) + + $result = Get-AzDoGitPermission -ProjectName $ProjectName -RepositoryName $RepositoryName -isInherited $isInherited -Permissions $Permissions + + $result | Should -Not -BeNullOrEmpty + $result.status | Should -Be 'NotFound' + Assert-VerifiableMock + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/New-AzDoGitPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/New-AzDoGitPermission.tests.ps1 new file mode 100644 index 000000000..1c7d03d99 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/New-AzDoGitPermission.tests.ps1 @@ -0,0 +1,136 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'New-AzDoGitPermission' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-AzDoGitPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + . (Get-ClassFilePath '002.LocalizedDataAzSerializationPatten') + + Mock -CommandName Get-CacheItem -MockWith { return @{ namespaceId = '12345'; id = '67890' } } + Mock -CommandName ConvertTo-ACLHashtable -MockWith { return @{} } + Mock -CommandName Set-AzDoPermission -MockWith { } + } + + Context 'With mandatory parameters provided' { + It 'should call Get-CacheItem for SecurityNamespace and Project' { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + isInherited = $true + } + New-AzDoGitPermission @params + + Assert-MockCalled -CommandName Set-AzDoPermission -Exactly 1 + } + + It 'should call ConvertTo-ACLHashtable and Set-AzDoPermission' { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + isInherited = $true + LookupResult = @{ propertiesChanged = @{} } + } + New-AzDoGitPermission @params + + Assert-MockCalled -CommandName ConvertTo-ACLHashtable -Exactly 1 + Assert-MockCalled -CommandName Set-AzDoPermission -Exactly 1 + } + } + + Context 'With all parameters provided' { + It 'should set permissions correctly' { + $permissions = @(@{ Permission = 'Read'; Access = 'Allow' }) + + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + isInherited = $true + Permissions = $permissions + LookupResult = @{ propertiesChanged = @{} } + Ensure = 'Present' + Force = $true + } + New-AzDoGitPermission @params + + Assert-MockCalled -CommandName Get-CacheItem -Times 2 + Assert-MockCalled -CommandName ConvertTo-ACLHashtable -Exactly 1 + Assert-MockCalled -CommandName Set-AzDoPermission -Exactly 1 + } + } + + Context 'When Get-CacheItem returns nothing' { + It 'should not call ConvertTo-ACLHashtable or Set-AzDoPermission' { + + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'SecurityNamespaces' } -MockWith { return $null } + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'LiveProjects' } -MockWith { return $null } + Mock -CommandName Write-Warning -Verifiable + + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + isInherited = $true + } + New-AzDoGitPermission @params + + Assert-MockCalled -CommandName ConvertTo-ACLHashtable -Exactly 0 + Assert-MockCalled -CommandName Set-AzDoPermission -Exactly 0 + Assert-VerifiableMock + } + } + + # Not in use + Context 'When Force switch is provided' -skip { + It 'should handle the Force switch correctly' { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + isInherited = $true + Force = $true + } + New-AzDoGitPermission @params + + # Verify if any additional logic related to -Force was executed + # This is a placeholder as the current implementation does not use -Force + } + } + + Context 'Verbose output' { + It 'should write verbose output' { + + Mock -CommandName Write-Verbose -Verifiable + + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + isInherited = $true + } + + New-AzDoGitPermission @params + + Assert-VerifiableMock + + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Remove-AzDoGitPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Remove-AzDoGitPermission.tests.ps1 new file mode 100644 index 000000000..d68d2821e --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Remove-AzDoGitPermission.tests.ps1 @@ -0,0 +1,158 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Remove-AzDoGitPermission" { + + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoGitPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + . (Get-ClassFilePath '002.LocalizedDataAzSerializationPatten') + + Mock -CommandName Remove-AzDoPermission + Mock -CommandName Get-CacheItem -MockWith { + switch ($Type) + { + 'SecurityNamespaces' { @{ namespaceId = 'namespaceIdValue' } } + 'LiveProjects' { @{ id = 'projectIdValue' } } + 'LiveRepositories' { @{ id = 'repositoryIdValue' } } + 'LiveACLList' { @(@{ token = 'repoV2/projectIdValue/repositoryIdValue' }) } + default { $null } + } + } + + $Global:DSCAZDO_OrganizationName = 'TestOrg' + + } + + BeforeEach { + + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + isInherited = $false + Permissions = @() + LookupResult = @{} + Ensure = 'Present' + Force = $false + } + + } + + It "Removes ACLs if Filtered is not null" { + + Mock -CommandName 'Write-Verbose' -Verifiable + + Remove-AzDoGitPermission @params + + Assert-MockCalled -CommandName Remove-AzDoPermission -Times 1 + Assert-VerifiableMock + + } + + It "Does not call Remove-GitRepositoryPermission if Filtered is null" { + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Get-CacheItem -MockWith { + switch ($Type) { + 'LiveACLList' { @(@{ token = 'repoV2/notMatchingValue' }) } + default { $null } + } + } + + Remove-AzDoGitPermission @params + + Assert-MockCalled -CommandName Remove-AzDoPermission -Exactly 0 + Assert-VerifiableMock + + } + + It "Does not call Remove-GitRepositoryPermission if ACLs are null" { + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Get-CacheItem -MockWith { + switch ($Type) { + 'LiveACLList' { $null } + default { $null } + } + } + + Remove-AzDoGitPermission @params + + Assert-MockCalled -CommandName Remove-AzDoPermission -Exactly 0 + Assert-VerifiableMock + + } + + It "Does not call Remove-GitRepositoryPermission if Repository is null" { + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Get-CacheItem -MockWith { + switch ($Type) { + 'LiveRepositories' { $null } + default { $null } + } + } + + Remove-AzDoGitPermission @params + + Assert-MockCalled -CommandName Remove-AzDoPermission -Exactly 0 + Assert-VerifiableMock + + } + + It "Does not call Remove-GitRepositoryPermission if Project is null" { + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Get-CacheItem -MockWith { + switch ($Type) { + 'LiveProjects' { $null } + default { $null } + } + } + + Remove-AzDoGitPermission @params + + Assert-MockCalled -CommandName Remove-AzDoPermission -Exactly 0 + Assert-VerifiableMock + + } + + It "Does not call Remove-GitRepositoryPermission if SecurityNamespace is null" { + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Get-CacheItem -MockWith { + switch ($Type) { + 'SecurityNamespaces' { $null } + default { $null } + } + } + + Remove-AzDoGitPermission @params + + Assert-MockCalled -CommandName Remove-AzDoPermission -Exactly 0 + Assert-VerifiableMock + + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Set-AzDoGitPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Set-AzDoGitPermission.tests.ps1 new file mode 100644 index 000000000..31660a606 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitPermission/Set-AzDoGitPermission.tests.ps1 @@ -0,0 +1,107 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-AzDoGitPermission' { + + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoGitPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + . (Get-ClassFilePath '002.LocalizedDataAzSerializationPatten') + + Mock -CommandName Get-CacheItem -MockWith { return @{ namespaceId = 'SampleNamespaceId' } } + Mock -CommandName ConvertTo-ACLHashtable -MockWith { return 'SerializedACLs' } + Mock -CommandName Set-AzDoPermission + + $ProjectName = 'TestProject' + $RepositoryName = 'TestRepo' + $isInherited = $true + $Permissions = @(@{ User = 'TestUser'; Permission = 'Allow' }) + $LookupResult = @{ propertiesChanged = 'someValue' } + $Ensure = [Ensure]::Present + + $params = @{ + ProjectName = $ProjectName + RepositoryName = $RepositoryName + isInherited = $isInherited + Permissions = $Permissions + LookupResult = $LookupResult + Ensure = $Ensure + } + + $Global:DSCAZDO_OrganizationName = 'TestOrg' + + } + + BeforeEach { + + Mock Get-CacheItem -MockWith { + return @{ + namespaceId = 'SampleNamespaceId' + } + } + Mock ConvertTo-ACLHashtable -MockWith { + return 'SerializedACLs' + } + Mock Set-AzDoPermission -MockWith { + return $null + } + + } + + It 'Calls Get-CacheItem with the correct parameters for security namespace' { + Set-AzDoGitPermission @params + Assert-MockCalled Get-CacheItem -Exactly 1 -ParameterFilter { ($Key -eq 'Git Repositories') -and ($Type -eq 'SecurityNamespaces') } + } + + It 'Calls Get-CacheItem with the correct parameters for the project' { + Set-AzDoGitPermission @params + Assert-MockCalled Get-CacheItem -Exactly 1 -ParameterFilter { ($Key -eq $ProjectName) -and ($Type -eq 'LiveProjects') } + } + + It 'Calls Set-AzDoPermission with the correct parameters' { + Set-AzDoGitPermission @params + Assert-MockCalled Set-AzDoPermission -Exactly 1 -ParameterFilter { + ($OrganizationName -eq 'TestOrg') -and + ($SecurityNamespaceID -eq 'SampleNamespaceId') -and + ($SerializedACLs -eq 'SerializedACLs') + } + } + + It 'Serializes ACLs using ConvertTo-ACLHashtable with correct parameters' { + Set-AzDoGitPermission @params + Assert-MockCalled ConvertTo-ACLHashtable -Exactly 1 -ParameterFilter { + $ReferenceACLs -eq 'someValue' + } + } + + It 'writes an error if Get-CacheItem is null' { + + Mock Get-CacheItem -MockWith { return $null } + Mock Write-Error -Verifiable + + Set-AzDoGitPermission @params + Assert-VerifiableMock + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Get-AzDoGitRepository.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Get-AzDoGitRepository.tests.ps1 new file mode 100644 index 000000000..fa315ec0f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Get-AzDoGitRepository.tests.ps1 @@ -0,0 +1,90 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Get-AzDoGitRepository Tests" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoGitRepository.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + + Mock -CommandName Get-CacheItem -MockWith { + return @{ RepositoryName = $repositoryName } + } + + } + + Context "When repository exists in the live cache" { + + It "should return repository from the live cache with Unchanged status" { + $projectName = "TestProject" + $repositoryName = "TestRepository" + $projectGroupKey = "$projectName\" + + Mock -CommandName Get-CacheItem -MockWith { + return @{ RepositoryName = $repositoryName } + } -ParameterFilter { + $Type -eq 'LiveRepositories' + } + + $result = Get-AzDoGitRepository -ProjectName $projectName -RepositoryName $repositoryName + + $result.status | Should -Be "Unchanged" + $result.Ensure | Should -Be "Absent" + } + } + + Context "When repository does not exist in the live cache" { + + It "should perform a lookup within the local cache" -skip { + $projectName = "TestProject" + $repositoryName = "TestRepository" + $projectGroupKey = "$projectName\" + + Mock -CommandName Get-CacheItem -MockWith { + return $null + } -ParameterFilter { + ($Key -eq $projectGroupKey) -and ($Type -eq 'Repositories') + } + + $result = Get-AzDoGitRepository -ProjectName $projectName -RepositoryName $repositoryName + + Assert-MockCalled -CommandName Get-CacheItem -Times 2 -Exactly + } + + It "should return NotFound status" { + $projectName = "TestProject" + $repositoryName = "TestRepository" + $projectGroupKey = "$projectName\" + + Mock -CommandName Get-CacheItem -ParameterFilter { + $Type -eq 'LiveRepositories' + } + + $result = Get-AzDoGitRepository -ProjectName $projectName -RepositoryName $repositoryName + + $result.status | Should -Be "NotFound" + $result.Ensure | Should -Be "Absent" + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/New-AzDoGitRepository.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/New-AzDoGitRepository.tests.ps1 new file mode 100644 index 000000000..c66f92e91 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/New-AzDoGitRepository.tests.ps1 @@ -0,0 +1,137 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Pester tests for New-AzDoGitRepository function +Describe "New-AzDoGitRepository Tests" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-AzDoGitRepository.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mock external cmdlets/functions + Mock -CommandName Get-CacheItem -MockWith { return @{ Name = "TestProject" } } + Mock -CommandName New-GitRepository -MockWith { return @{ Name = $RepositoryName } } + Mock -CommandName Add-CacheItem + Mock -CommandName Export-CacheObject + Mock -CommandName Refresh-CacheObject + + } + + Context "When mandatory parameters are provided" { + + BeforeEach { + $Global:DSCAZDO_OrganizationName = "TestOrg" + } + + It "should call New-GitRepository" { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + } + New-AzDoGitRepository @params + + Assert-MockCalled -CommandName New-GitRepository -Exactly -Times 1 + } + + It "should call Add-CacheItem" { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + } + New-AzDoGitRepository @params + + Assert-MockCalled -CommandName Add-CacheItem -Exactly -Times 1 + } + + It "should call Export-CacheObject" { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + } + New-AzDoGitRepository @params + + Assert-MockCalled -CommandName Export-CacheObject -Exactly -Times 1 + } + + It "should call Refresh-CacheObject" { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + } + New-AzDoGitRepository @params + + Assert-MockCalled -CommandName Refresh-CacheObject -Exactly -Times 1 + } + + } + + Context "When optional parameters are provided" { + + It "should pass SourceRepository to New-GitRepository" { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + SourceRepository= 'SourceRepo' + } + New-AzDoGitRepository @params + + Assert-MockCalled -CommandName New-GitRepository -ParameterFilter { $RepositoryName -eq 'TestRepo' -and $SourceRepository -eq 'SourceRepo' } + } + + It "should handle Force switch parameter" -skip { + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + Force = $true + } + New-AzDoGitRepository @params + + # Since Force is not used in function logic directly, verifying other aspects + Assert-MockCalled -CommandName New-GitRepository -Exactly -Times 1 + } + } + + Context 'When the cache returns $null' { + + BeforeEach { + Mock -CommandName Get-CacheItem -MockWith { return $null } + } + + It "should process the repository creation" { + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Get-CacheItem -MockWith { return $null } + + $params = @{ + ProjectName = 'TestProject' + RepositoryName = 'TestRepo' + } + New-AzDoGitRepository @params + + Assert-VerifiableMock + Assert-MockCalled -CommandName New-GitRepository -Exactly -Times 0 + } + + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Remove-AzDoGitRepository.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Remove-AzDoGitRepository.tests.ps1 new file mode 100644 index 000000000..ed28b80b6 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Remove-AzDoGitRepository.tests.ps1 @@ -0,0 +1,100 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-AzDoGitRepository' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoGitRepository.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + Mock -CommandName Get-CacheItem -MockWith { + return @{ + Key = "$ProjectName\" + Value = "RepositoryValue" + } + } + + Mock -CommandName Remove-GitRepository -MockWith { + return @{ + Name = $RepositoryName + } + } + + Mock -CommandName Remove-CacheItem + Mock -CommandName Export-CacheObject + + $params = @{ + ProjectName = "TestProject" + RepositoryName = "TestRepository" + Ensure = "Present" + } + + } + + It 'Calls Get-CacheItem with appropriate parameters for Project' { + Remove-AzDoGitRepository @params + + Assert-MockCalled -CommandName Get-CacheItem -Times 1 -Exactly -ParameterFilter { + $Key -eq "TestProject" -and $Type -eq "LiveProjects" + } + } + + It 'Calls Get-CacheItem with appropriate parameters for Repository' { + Remove-AzDoGitRepository @params + + Assert-MockCalled -CommandName Get-CacheItem -Times 1 -Exactly -ParameterFilter { + $Key -eq "TestProject\TestRepository" -and $Type -eq "LiveRepositories" + } + } + + It 'Calls Remove-GitRepository with appropriate parameters' { + Remove-AzDoGitRepository @params + Assert-MockCalled -CommandName Remove-GitRepository -Exactly 1 + } + + It 'Calls Remove-CacheItem with appropriate parameters' { + Remove-AzDoGitRepository @params + + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -Exactly -ParameterFilter { + $Key -eq "TestProject\TestRepository" -and $Type -eq "LiveRepositories" + } + } + + It 'Calls Export-CacheObject with appropriate parameters' { + Remove-AzDoGitRepository @params + + Assert-MockCalled -CommandName Export-CacheObject -Times 1 -Exactly -ParameterFilter { + $CacheType -eq 'LiveRepositories' -and $Content -eq $AzDoLiveRepositories + } + } + + It 'Fails if Project does not exist in LiveProjects cache' { + + Mock -CommandName Write-Error -Verifiable + Mock -CommandName Get-CacheItem -MockWith { return $null } -ParameterFilter { $Type -eq 'LiveProjects' } + + Remove-AzDoGitRepository @params | Should -BeNullOrEmpty + Assert-VerifiableMock + + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Set-AzDoGitRepository.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Set-AzDoGitRepository.tests.ps1 new file mode 100644 index 000000000..f224fd85b --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/Set-AzDoGitRepository.tests.ps1 @@ -0,0 +1,25 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-AzDoGitRepository' -Skip { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoGitRepository.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Get-AzDoGroupMember.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Get-AzDoGroupMember.tests.ps1 new file mode 100644 index 000000000..513c79d44 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Get-AzDoGroupMember.tests.ps1 @@ -0,0 +1,160 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Get-AzDoGroupMember Tests" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mock dependencies + Mock -CommandName Format-AzDoGroupMember -MockWith { return "MockKey" } + Mock -CommandName Get-CacheItem -MockWith { + return @( + [PSCustomObject]@{ originId = "MockMember1" }, + [PSCustomObject]@{ originId = "MockMember2" } + ) + } + + Mock -CommandName Find-AzDoIdentity -MockWith { + param ([string]$Identity) + return [PSCustomObject]@{ originId = $Identity } + } + + } + + It "Handles group not found in live cache and no group members in parameters" { + $params = @{ + GroupName = "TestGroup" + GroupMembers = @() + LookupResult = @{} + Ensure = "Absent" + Force = $false + } + + Mock -CommandName Get-CacheItem -MockWith { $null } + + $result = Get-AzDoGroupMember @params + $result.status | Should -Be ([DSCGetSummaryState]::Unchanged) + } + + It "Handles group members in parameters but not found in live cache" { + $params = @{ + GroupName = "TestGroup" + GroupMembers = @("Member1") + LookupResult = @{} + Ensure = "Absent" + Force = $false + } + + Mock -CommandName Get-CacheItem -MockWith { $null } + + $result = Get-AzDoGroupMember @params + $result.status | Should -Be ([DSCGetSummaryState]::NotFound) + } + + It "Handles group members not found in live cache but are defined in parameters" { + $params = @{ + GroupName = "TestGroup" + GroupMembers = @("Member1", "Member2") + LookupResult = @{} + Ensure = "Absent" + Force = $false + } + + Mock -CommandName Get-CacheItem -MockWith { $null } + + $result = Get-AzDoGroupMember @params + $result.status | Should -Be ([DSCGetSummaryState]::NotFound) + } + + It "Handles no group members in parameters but live cache has members" { + $params = @{ + GroupName = "TestGroup" + GroupMembers = @() + LookupResult = @{} + Ensure = "Absent" + Force = $false + } + + $result = Get-AzDoGroupMember @params + $result.status | Should -Be ([DSCGetSummaryState]::Missing) + } + + It "Handles same group members in parameters and live cache" { + + Mock -CommandName Compare-Object + + $params = @{ + GroupName = "TestGroup" + GroupMembers = @("MockMember1", "MockMember2") + LookupResult = @{} + Ensure = "Absent" + Force = $false + } + + $result = Get-AzDoGroupMember @params + $result.status | Should -Be ([DSCGetSummaryState]::Unchanged) + + } + + It "Handles different members in live cache and parameters" { + + $params = @{ + GroupName = "TestGroup" + GroupMembers = @("MockMember1", "NewMember") + LookupResult = @{} + Ensure = "Absent" + Force = $false + } + + $result = Get-AzDoGroupMember @params + $result.status | Should -Be ([DSCGetSummaryState]::Changed) + $result.propertiesChanged[0].action | Should -Be 'Remove' + $result.propertiesChanged[0].value.originId | Should -Be 'MockMember2' + $result.propertiesChanged[1].action | Should -Be 'Add' + $result.propertiesChanged[1].value.originId | Should -Be 'NewMember' + + } + + # -Force is not used in the function logic directly. + It "Handles different members in live cache and parameters with Force" -skip { + + $params = @{ + GroupName = "TestGroup" + GroupMembers = @("MockMember1", "NewMember") + LookupResult = @{} + Ensure = "Absent" + Force = $true + } + + $result = Get-AzDoGroupMember @params + $result.status | Should -Be ([DSCGetSummaryState]::Changed) + $result.propertiesChanged[0].action | Should -Be 'Remove' + $result.propertiesChanged[0].value.originId | Should -Be 'MockMember2' + $result.propertiesChanged[1].action | Should -Be 'Add' + $result.propertiesChanged[1].value.originId | Should -Be 'NewMember' + + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/New-AzDoGroupMember.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/New-AzDoGroupMember.tests.ps1 new file mode 100644 index 000000000..bceae1cde --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/New-AzDoGroupMember.tests.ps1 @@ -0,0 +1,118 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "New-AzDoGroupMember" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + Remove-Variable -Name AzDoLiveGroupMembers -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + $global:AzDoLiveGroupMembers = @{} + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-AzDoGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + + Mock -CommandName Find-AzDoIdentity -MockWith { + param ($Identity) + return [PSCustomObject]@{ + displayName = $Identity + originId = [guid]::NewGuid().ToString() + principalName = 'mockPrincipalName' + } + } + + Mock -CommandName Get-CacheObject -MockWith { + param ($CacheType) + return @() + } + + Mock -CommandName New-DevOpsGroupMember -MockWith { + param ($params, $MemberIdentity) + return $MemberIdentity + } + + Mock -CommandName Add-CacheItem + Mock -CommandName Set-CacheObject + + } + + Context "When valid parameters are passed" { + + It "Should call Find-AzDoIdentity for the group name" { + $GroupName = 'TestGroup' + $GroupMembers = @('User1', 'User2') + + New-AzDoGroupMember -GroupName $GroupName -GroupMembers $GroupMembers + + Assert-MockCalled -CommandName Find-AzDoIdentity -Times 3 -Exactly -Scope It + } + + It "Should add members to the group" { + $GroupName = 'TestGroup' + $GroupMembers = @('User1', 'User2') + + New-AzDoGroupMember -GroupName $GroupName -GroupMembers $GroupMembers + + Assert-MockCalled -CommandName New-DevOpsGroupMember -Times 2 -Exactly -Scope It + } + + It "Should cache group members" { + $GroupName = 'TestGroup' + $GroupMembers = @('User1', 'User2') + + New-AzDoGroupMember -GroupName $GroupName -GroupMembers $GroupMembers + + Assert-MockCalled -CommandName Add-CacheItem -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -Exactly -Scope It + } + } + + Context "When no members are found" { + + BeforeAll { + Mock -CommandName 'Find-AzDoIdentity' -MockWith { + param ($Identity) + return $null + } + } + + It "Should write an error when no identities are found" { + Mock -CommandName Write-Warning + $GroupName = 'TestGroup' + $GroupMembers = @('User1', 'User2') + + $result = New-AzDoGroupMember -GroupName $GroupName -GroupMembers $GroupMembers + Assert-MockCalled -Times 1 -ParameterFilter { $Message -like "*Unable to find identity for member*" } -CommandName Write-Warning + } + + It "Should write a warning when no members are found" { + Mock -CommandName Write-Warning + Mock -CommandName Find-AzDoIdentity -ParameterFilter { ($Identity -eq 'User1') -or ($Identity -eq 'User2') } + $GroupName = 'TestGroup' + $GroupMembers = @('User1', 'User2') + + New-AzDoGroupMember -GroupName $GroupName -GroupMembers $GroupMembers + + Assert-MockCalled -CommandName 'Write-Warning' -ParameterFilter { $Message -like "*No group members found*" } + + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Remove-AzDoGroupMember.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Remove-AzDoGroupMember.tests.ps1 new file mode 100644 index 000000000..e2968d10d --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Remove-AzDoGroupMember.tests.ps1 @@ -0,0 +1,139 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-AzDoGroupMember Tests' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + Remove-Variable -Name AzDoLiveGroupMembers -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + $global:AzDoLiveGroupMembers = @{} + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + Mock -CommandName Find-AzDoIdentity -MockWith { + return @{ principalName = 'mockUser@domain.com' } + } + + Mock -CommandName Get-CacheItem -MockWith { + return @( + @{ principalName = 'userA@domain.com' }, + @{ principalName = 'userB@domain.com' } + ) + } + + Mock -CommandName Remove-DevOpsGroupMember -MockWith { + return @{ Result = 'Success' } + } + + Mock -CommandName Write-Warning + Mock -CommandName Remove-CacheItem + Mock -CommandName Set-CacheObject + Mock -CommandName Format-AzDoProjectName -MockWith { return '[TestProjectName]\GroupName' } + + } + + BeforeEach { + $Global:DSCAZDO_OrganizationName = 'MockOrganization' + $Global:AzDoLiveGroupMembers = 'MockMembers' + } + + Context "when functioning correctly" { + It 'Should remove group members correctly' { + $GroupName = 'TestGroup' + $result = Remove-AzDoGroupMember -GroupName $GroupName + + Assert-MockCalled -CommandName Find-AzDoIdentity -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Times 1 + Assert-MockCalled -CommandName Remove-DevOpsGroupMember -Times 2 + + } + + It 'Should update the cache' { + Mock -CommandName Write-Warning + + $GroupName = 'TestGroup' + $result = Remove-AzDoGroupMember -GroupName $GroupName + + Assert-MockCalled -CommandName Write-Warning -ParameterFilter { $Message -like '*No group members found*'} -Exactly 0 + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 + Assert-MockCalled -CommandName Set-CacheObject -Times 1 + + } + + It 'Should handle an empty members list' { + Mock -CommandName Get-CacheItem -MockWith { + return @() + } + Mock -CommandName Write-Warning + + $GroupName = 'TestGroup' + $result = Remove-AzDoGroupMember -GroupName $GroupName + + Assert-MockCalled -CommandName Find-AzDoIdentity -Exactly 1 + Assert-MockCalled -CommandName Remove-DevOpsGroupMember -Exactly 0 + Assert-MockCalled -CommandName Remove-CacheItem -Exactly 1 + Assert-MockCalled -CommandName Set-CacheObject -Exactly 1 + + } + + It 'Should handle a bad members list' { + + $GroupName = 'TestGroup' + + Mock -CommandName Find-AzDoIdentity -MockWith { + return @(' ', $null) + } -ParameterFilter { $Identity -ne $GroupName } + + Mock -CommandName Write-Warning + + $result = Remove-AzDoGroupMember -GroupName $GroupName + + Assert-MockCalled -CommandName Write-Warning -ParameterFilter { $Message -like '*Unable to find identity*'} -Exactly 2 + Assert-MockCalled -CommandName Remove-DevOpsGroupMember -Exactly 0 + Assert-MockCalled -CommandName Remove-CacheItem -Exactly 1 + Assert-MockCalled -CommandName Set-CacheObject -Exactly 1 + } + + } + + Context "when failing" { + + It 'Should handle a missing group identity' { + Mock -CommandName Find-AzDoIdentity -MockWith { + return $null + } + + Mock -CommandName Write-Warning + + $GroupName = 'TestGroup' + $result = Remove-AzDoGroupMember -GroupName $GroupName + + Assert-MockCalled -CommandName Write-Warning -ParameterFilter { $Message -like '*Unable to find identity*'} -Exactly 1 + Assert-MockCalled -CommandName Remove-DevOpsGroupMember -Exactly 0 + Assert-MockCalled -CommandName Remove-CacheItem -Exactly 0 + Assert-MockCalled -CommandName Set-CacheObject -Exactly 0 + } + + } + + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Set-AzDoGroupMember.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Set-AzDoGroupMember.tests.ps1 new file mode 100644 index 000000000..e025df1d0 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Set-AzDoGroupMember.tests.ps1 @@ -0,0 +1,228 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-AzDoGroupMember' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + Remove-Variable -Name AzDoLiveGroupMembers -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + $global:AzDoLiveGroupMembers = @{} + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + + Mock -CommandName Find-AzDoIdentity -MockWith { + return @{ principalName = "GroupName"; originId = "GroupOriginId"; displayName = "Test Group" } + } + Mock -CommandName Format-AzDoProjectName -MockWith { + return "FormattedProjectName" + } + Mock -CommandName Get-CacheItem -MockWith { + return @() + } + + Mock -CommandName Get-Cacheitem -ParameterFilter { $Type -eq 'LiveGroupMembers' } -MockWith { + return @( + @{ principalName = 'user1'; originId = 'user1OriginId'; displayName = 'User 1' }, + @{ principalName = 'user2'; originId = 'user2OriginId'; displayName = 'User 2' } + ) + } + + Mock -CommandName New-DevOpsGroupMember -MockWith { + return $true + } + Mock -CommandName Remove-DevOpsGroupMember -MockWith { + return $true + } + Mock -CommandName Add-CacheItem + Mock -CommandName Set-CacheObject + + $LookupResult = @{ + propertiesChanged = @( + @{ action = 'Add'; value = @{ principalName = 'user1'; originId = 'user1OriginId'; displayName = 'User 1' } }, + @{ action = 'Remove'; value = @{ principalName = 'user2'; originId = 'user2OriginId'; displayName = 'User 2' } } + ) + } + + } + + Context 'When adding a group member' { + + It 'Should call New-DevOpsGroupMember with correct parameters' { + + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + $params = @{ + GroupIdentity = @{ + principalName = "GroupName"; + originId = "GroupOriginId"; + displayName = "Test Group" + } + ApiUri = 'https://vssps.dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + MemberIdentity = @{ + principalName = 'user1'; + originId = 'user1OriginId'; + displayName = 'User 1' + } + } + + Assert-MockCalled -CommandName 'New-DevOpsGroupMember' -Exactly 1 + } + + It 'Should call Remove-DevOpsGroupMember with correct parameters' { + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + $params = @{ + GroupIdentity = @{ + principalName = "GroupName"; + originId = "GroupOriginId"; + displayName = "Test Group" + } + ApiUri = 'https://vssps.dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + MemberIdentity = @{ + principalName = 'user2'; + originId = 'user2OriginId'; + displayName = 'User 2' + } + } + + Assert-MockCalled -CommandName 'Remove-DevOpsGroupMember' -Exactly 1 + } + + it "should add and remove members" { + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + $params = @{ + GroupIdentity = @{ + principalName = "GroupName"; + originId = "GroupOriginId"; + displayName = "Test Group" + } + ApiUri = 'https://vssps.dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + MemberIdentity = @{ + principalName = 'user1'; + originId = 'user1OriginId'; + displayName = 'User 1' + } + } + + Assert-MockCalled -CommandName 'New-DevOpsGroupMember' -Exactly 1 + + $params = @{ + GroupIdentity = @{ + principalName = "GroupName"; + originId = "GroupOriginId"; + displayName = "Test Group" + } + ApiUri = 'https://vssps.dev.azure.com/{0}/' -f $Global:DSCAZDO_OrganizationName + MemberIdentity = @{ + principalName = 'user2'; + originId = 'user2OriginId'; + displayName = 'User 2' + } + } + + Assert-MockCalled -CommandName 'Remove-DevOpsGroupMember' -Exactly 1 + + } + + } + + Context "when a circular reference is detected" { + + it "should ignore adding the member" { + + Mock Write-Warning -Verifiable + + $LookupResult = @{ + propertiesChanged = @( + @{ action = 'Add'; value = @{ principalName = 'user1'; originId = 'GroupOriginId'; displayName = 'User 1' } }, + @{ action = 'Remove'; value = @{ principalName = 'user2'; originId = 'GroupOriginId'; displayName = 'User 2' } } + ) + } + + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + Assert-MockCalled -CommandName 'New-DevOpsGroupMember' -Exactly 0 + Assert-MockCalled -CommandName 'Remove-DevOpsGroupMember' -Exactly 0 + Assert-VerifiableMock + + } + + it "should ignore removing the member" { + + Mock Write-Warning -Verifiable + + $LookupResult = @{ + propertiesChanged = @( + @{ action = 'Add'; value = @{ principalName = 'user1'; originId = 'GroupOriginId'; displayName = 'User 1' } }, + @{ action = 'Remove'; value = @{ principalName = 'user2'; originId = 'GroupOriginId'; displayName = 'User 2' } } + ) + } + + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + Assert-MockCalled -CommandName 'New-DevOpsGroupMember' -Exactly 0 + Assert-MockCalled -CommandName 'Remove-DevOpsGroupMember' -Exactly 0 + Assert-VerifiableMock + + } + + } + + Context "when functions called return '`$null'" { + + it "should not start when Get-CacheItem returns `$null" { + + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'LiveGroupMembers' } + Mock -CommandName Write-Error + + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + Assert-MockCalled -CommandName 'Write-Error' -Exactly 1 -ParameterFilter { $Message -like '*LiveGroupMembers cache for group*' } + Assert-MockCalled -CommandName 'New-DevOpsGroupMember' -Exactly 0 + Assert-MockCalled -CommandName 'Remove-DevOpsGroupMember' -Exactly 0 + + } + + it "should not call New-DevOpsGroupMember" { + + Mock -CommandName New-DevOpsGroupMember -MockWith { return $null } + + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + Assert-MockCalled -CommandName 'New-DevOpsGroupMember' -Exactly 1 + + } + + it "should not call Remove-DevOpsGroupMember" { + + Mock -CommandName Remove-DevOpsGroupMember -MockWith { return $null } + + Set-AzDoGroupMember -GroupName 'TestGroup' -LookupResult $LookupResult -Ensure 'Present' + + Assert-MockCalled -CommandName 'Remove-DevOpsGroupMember' -Exactly 1 + + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Test-AzDoGroupMember.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Test-AzDoGroupMember.tests.ps1 new file mode 100644 index 000000000..40609976c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupMember/Test-AzDoGroupMember.tests.ps1 @@ -0,0 +1,79 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Test-AzDoGroupMember' -skip { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + Remove-Variable -Name AzDoLiveGroupMembers -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + $global:AzDoLiveGroupMembers = @{} + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDoGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + $GroupName = "TestGroup" + $GroupMembers = @('User1', 'User2') + $LookupResult = @{ User1 = 'ID1'; User2 = 'ID2' } + $Ensure = 'Present' + $Force = $true + + } + + It 'Should accept mandatory GroupName parameter' { + { Test-AzDoGroupMember -GroupName $GroupName } | Should -Not -Throw + } + + It 'Should accept optional GroupMembers parameter' { + { Test-AzDoGroupMember -GroupName $GroupName -GroupMembers $GroupMembers } | Should -Not -Throw + } + + It 'Should accept optional LookupResult parameter' { + { Test-AzDoGroupMember -GroupName $GroupName -LookupResult $LookupResult } | Should -Not -Throw + } + + It 'Should accept optional Ensure parameter' { + { Test-AzDoGroupMember -GroupName $GroupName -Ensure $Ensure } | Should -Not -Throw + } + + It 'Should accept Force switch parameter' { + { Test-AzDoGroupMember -GroupName $GroupName -Force } | Should -Not -Throw + { Test-AzDoGroupMember -GroupName $GroupName -Force:$false } | Should -Not -Throw + } + + It 'Should fail without mandatory GroupName parameter' { + { Test-AzDoGroupMember } | Should -Throw + } + + It 'Should handle null or empty GroupMembers parameter' { + { Test-AzDoGroupMember -GroupName $GroupName -GroupMembers $null } | Should -Not -Throw + { Test-AzDoGroupMember -GroupName $GroupName -GroupMembers @() } | Should -Not -Throw + } + + It 'Should handle null or empty LookupResult parameter' { + { Test-AzDoGroupMember -GroupName $GroupName -LookupResult $null } | Should -Not -Throw + { Test-AzDoGroupMember -GroupName $GroupName -LookupResult @{} } | Should -Not -Throw + } + + It 'Should return the expected result' { + $result = Test-AzDoGroupMember -GroupName $GroupName -GroupMembers $GroupMembers -LookupResult $LookupResult -Ensure $Ensure -Force + $result | Should -Be $return + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Get-AzDoGroupPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Get-AzDoGroupPermission.tests.ps1 new file mode 100644 index 000000000..a92953e0f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Get-AzDoGroupPermission.tests.ps1 @@ -0,0 +1,137 @@ +$currentFile = $MyInvocation.MyCommand.Path + + +# Tests are currently disabled. +Describe 'Get-AzDoGroupPermission' -skip { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + + + # Mock dependencies + Mock -CommandName Get-CacheItem -MockWith { + + switch ($Type) { + 'LiveGroups' { + return @{ + id = 'mockOriginId' + name = 'mockOriginName' + } + } + 'LiveProjects' { + return @{ + id = 'mockProjectId' + name = 'mockProjectName' + } + } + 'SecurityNamespaces' { + return @{ + namespaceId = 'mockSecurityNamespaceId' + name = 'mockSecurityNamespaceName' + } + } + } + } + + + Mock -CommandName Get-DevOpsACL -MockWith { + return @( + @{ + Token = @{ + Type = 'GroupPermission' + GroupId = 'mockOriginId' + ProjectId= 'mockProjectId' + } + } + ) + } + + Mock -CommandName ConvertTo-FormattedACL -MockWith { + return @( + @{ + Token = @{ + Type = 'GroupPermission' + GroupId = 'mockOriginId' + ProjectId= 'mockProjectId' + } + } + ) + } + + Mock -CommandName ConvertTo-ACL -MockWith { + return @{ + aces = @{ + Count = 1 + } + token = @{ + Type = 'GroupPermission' + } + } + } + + Mock -CommandName Test-ACLListforChanges -MockWith { + return @{ + propertiesChanged = @('property1', 'property2') + status = 'Compliant' + reason = 'No changes detected' + } + } + + Mock -CommandName Write-Warning + + } + + It 'Should return group result with correct properties when valid GroupName is provided' { + + $result = Get-AzDoGroupPermission -GroupName 'Project\Group' -isInherited $true + + $result | Should -Not -BeNullOrEmpty + $result.project | Should -Be 'Project' + $result.groupName | Should -Be 'Group' + $result.propertiesChanged | Should -Contain 'property1' + $result.status | Should -Be 'Unchanged' + + } + + It 'Should not throw an error when GroupName is invalid' { + $result = Get-AzDoGroupPermission -GroupName 'InvalidGroupName' -isInherited $true + $result | Should -BeNullOrEmpty + } + + It 'Should return null when no ACEs found for the group' { + Mock -CommandName 'ConvertTo-ACL' -MockWith { + param ($Permissions, $SecurityNamespace, $isInherited, $OrganizationName, $TokenName) + return @{ + aces = @{ + Count = 0 + } + } + } + + $result = Get-AzDoGroupPermission -GroupName 'Project\Group' -isInherited $true + $result | Should -BeNullOrEmpty + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/New-AzDoGroupPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/New-AzDoGroupPermission.tests.ps1 new file mode 100644 index 000000000..1615509d0 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/New-AzDoGroupPermission.tests.ps1 @@ -0,0 +1,120 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Resource is currently disabled +Describe 'New-AzDoGroupPermission' -skip { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoGroupMember.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mock dependencies + Mock -CommandName Get-CacheItem -MockWith { + param ($Key, $Type) + switch ($Type) + { + 'SecurityNamespaces' { + return @{ namespaceId = 'mockNamespaceId' } + } + 'LiveProjects' { + return @{ id = 'mockProjectId' } + } + 'LiveGroups' { + return @{ id = 'mockGroupId' } + } + 'LiveACLList' { + return @{} + } + default { + return $null + } + } + } + + Mock -CommandName ConvertTo-ACLHashtable -MockWith { + param ($ReferenceACLs, $DescriptorACLList, $DescriptorMatchToken) + return @{ + aces = @{ + Count = 1 + } + } + } + + Mock -CommandName Set-AzDoPermission + + } + + It 'Should set permissions when valid GroupName is provided' { + $LookupResult = @{ + propertiesChanged = @('property1', 'property2') + } + $Permissions = @( + @{ + PermissionBit = 'Read' + DisplayName = 'Read' + } + ) + + New-AzDoGroupPermission -GroupName 'Project\Group' -isInherited $true -Permissions $Permissions -LookupResult $LookupResult -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'Identity'; Type = 'SecurityNamespaces' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'Project'; Type = 'LiveProjects' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = '[Project]\Group'; Type = 'LiveGroups' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'mockNamespaceId'; Type = 'LiveACLList' } -Times 1 + Assert-MockCalled -CommandName ConvertTo-ACLHashtable -Times 1 + Assert-MockCalled -CommandName Set-AzDoPermission -Times 1 + } + + It 'Should throw a warning when GroupName is invalid' { + { New-AzDoGroupPermission -GroupName 'InvalidGroupName' -isInherited $true } | Should -Throw + } + + It 'Should handle case where no LookupResult is provided' { + $result = New-AzDoGroupPermission -GroupName 'Project\Group' -isInherited $true -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'Identity'; Type = 'SecurityNamespaces' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'Project'; Type = 'LiveProjects' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = '[Project]\Group'; Type = 'LiveGroups' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'mockNamespaceId'; Type = 'LiveACLList' } -Times 1 + Assert-MockCalled -CommandName ConvertTo-ACLHashtable -Times 1 + Assert-MockCalled -CommandName Set-AzDoPermission -Times 1 + } + + It 'Should not call Set-AzDoPermission if no ACLs are found' { + Mock -CommandName ConvertTo-ACLHashtable -MockWith { + return @{ + aces = @{ + Count = 0 + } + } + } + + $LookupResult = @{ + propertiesChanged = @('property1', 'property2') + } + + New-AzDoGroupPermission -GroupName 'Project\Group' -isInherited $true -LookupResult $LookupResult -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Set-AzDoPermission -Times 0 + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Remove-AzDoGroupPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Remove-AzDoGroupPermission.tests.ps1 new file mode 100644 index 000000000..cd207a8d6 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Remove-AzDoGroupPermission.tests.ps1 @@ -0,0 +1,100 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Tests are currently disabled. +Describe 'Remove-AzDoGroupPermission' -skip { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoGroupPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + + # Mock dependencies + Mock -CommandName Get-CacheItem -MockWith { + param ($Key, $Type) + switch ($Type) { + 'SecurityNamespaces' { return @{ namespaceId = 'mockNamespaceId' } } + 'LiveProjects' { return @{ id = 'mockProjectId' } } + 'LiveRepositories' { return @{ id = 'mockRepositoryId' } } + 'LiveACLList' { return @( + @{ token = 'repoV2/mockProjectId/mockRepositoryId' }, + @{ token = 'repoV2/anotherProject/anotherRepo' } + ) } + default { return $null } + } + } + + Mock -CommandName Remove-AzDoPermission -MockWith {} + + } + + It 'Should remove permissions when valid GroupName is provided' { + Remove-AzDoGroupPermission -GroupName 'Project\Repository' -isInherited $true -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'Identity'; Type = 'SecurityNamespaces' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'Project'; Type = 'LiveProjects' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'Project\Repository'; Type = 'LiveRepositories' } -Times 1 + Assert-MockCalled -CommandName Get-CacheItem -Parameters @{ Key = 'mockNamespaceId'; Type = 'LiveACLList' } -Times 1 + Assert-MockCalled -CommandName Remove-AzDoPermission -Times 1 + } + + It 'Should throw a warning when GroupName is invalid' { + { Remove-AzDoGroupPermission -GroupName 'InvalidGroupName' -isInherited $true } | Should -Throw + } + + It 'Should handle case where no matching ACLs are found' { + Mock -CommandName Get-CacheItem -MockWith { + param ($Key, $Type) + if ($Type -eq 'LiveACLList') { + return @( + @{ token = 'repoV2/anotherProject/anotherRepo' } + ) + } + return @{ + namespaceId = 'mockNamespaceId' + id = 'mockProjectId' + } + } + + Remove-AzDoGroupPermission -GroupName 'Project\Repository' -isInherited $true -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Remove-AzDoPermission -Times 0 + } + + It 'Should not call Remove-AzDoPermission if no ACLs are found' { + Mock -CommandName Get-CacheItem -MockWith { + param ($Key, $Type) + if ($Type -eq 'LiveACLList') { + return @() + } + return @{ + namespaceId = 'mockNamespaceId' + id = 'mockProjectId' + } + } + + Remove-AzDoGroupPermission -GroupName 'Project\Repository' -isInherited $true -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Remove-AzDoPermission -Times 0 + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Set-AzDoGroupPermission.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Set-AzDoGroupPermission.tests.ps1 new file mode 100644 index 000000000..2c29b98ce --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGroupPermission/Set-AzDoGroupPermission.tests.ps1 @@ -0,0 +1,129 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Tests are currently disabled. +Describe 'Set-AzDoGroupPermission' -skip { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoGroupPermission.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + + # Mock dependencies + Mock -CommandName Get-CacheItem -MockWith { + param ( + [string]$Key, + [string]$Type + ) + switch ($Type) + { + 'SecurityNamespaces' { + return @{ namespaceId = 'mockNamespaceId' } + } + 'LiveProjects' { + return @{ id = 'mockProjectId' } + } + 'LiveACLList' { + return @( + @{ token = 'repoV2/mockProjectId/mockRepositoryId' }, + @{ token = 'repoV2/anotherProject/anotherRepo' } + ) + } + default { + return $null + } + } + } + + Mock -CommandName ConvertTo-ACLHashtable -MockWith { + param ( + [HashTable]$ReferenceACLs, + [Array]$DescriptorACLList, + [string]$DescriptorMatchToken + ) + return @{ serializedACLs = 'mockSerializedACLs' } + } + + Mock -CommandName Set-AzDoPermission + + } + + It 'Should throw a warning when GroupName is invalid' { + { Set-AzDoGroupPermission -GroupName 'InvalidGroupName' -isInherited $true } | Should -Throw + } + + It 'Should set permissions when valid GroupName is provided' { + $LookupResult = @{ + propertiesChanged = @{} + } + + Set-AzDoGroupPermission -GroupName 'Project\Repository' -isInherited $true -Permissions @{} -LookupResult $LookupResult -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Get-CacheItem -Exactly -Times 1 -Scope It -ParameterFilter { + $Key -eq 'Identity' -and $Type -eq 'SecurityNamespaces' + } + Assert-MockCalled -CommandName Get-CacheItem -Exactly -Times 1 -Scope It -ParameterFilter { + $Key -eq $ProjectName -and $Type -eq 'LiveProjects' + } + Assert-MockCalled -CommandName ConvertTo-ACLHashtable -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-AzDoPermission -Exactly -Times 1 -Scope It + } + + It 'Should call ConvertTo-ACLHashtable with correct parameters' { + $LookupResult = @{ + propertiesChanged = @{} + } + + Set-AzDoGroupPermission -GroupName 'Project\Repository' -isInherited $true -Permissions @{} -LookupResult $LookupResult -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName ConvertTo-ACLHashtable -Exactly -Times 1 -Scope It -ParameterFilter { + $ReferenceACLs -eq $LookupResult.propertiesChanged -and + $DescriptorACLList -eq (Get-CacheItem -Key 'mockNamespaceId' -Type 'LiveACLList') -and + $DescriptorMatchToken -eq ('repoV2/mockProjectId/mockRepositoryId') + } + } + + It 'Should not call Set-AzDoPermission if no ACLs are found' { + Mock -CommandName Get-CacheItem -MockWith { + param ( + [string]$Key, + [string]$Type + ) + if ($Type -eq 'LiveACLList') { + return @() + } + return @{ + namespaceId = 'mockNamespaceId' + id = 'mockProjectId' + } + } + + $LookupResult = @{ + propertiesChanged = @{} + } + + Set-AzDoGroupPermission -GroupName 'Project\Repository' -isInherited $true -Permissions @{} -LookupResult $LookupResult -Ensure 'Present' -Force:$true + + Assert-MockCalled -CommandName Set-AzDoPermission -Exactly -Times 0 -Scope It + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Get-AzDoOrganizationGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Get-AzDoOrganizationGroup.tests.ps1 new file mode 100644 index 000000000..a6c94aee8 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Get-AzDoOrganizationGroup.tests.ps1 @@ -0,0 +1,139 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-AzDoOrganizationGroup' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoOrganizationGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + Mock -CommandName Format-AzDoGroup -MockWith { return "[$Global:DSCAZDO_OrganizationName]_$GroupName" } + Mock -CommandName Get-CacheItem -MockWith { + switch($Type) { + 'LiveGroups' { + return @{ originId = '123'; description = 'Test Group'; name = 'TestGroup' } + } + 'Group' { + return @{ originId = '123'; description = 'Test Group'; name = 'TestGroup'} + } + } + } + + } + + Context 'When group is present in live cache and local cache with same originId' -skip { + BeforeAll { + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'LiveGroups' } -MockWith { + return @{ originId = '123'; description = 'Test Group'; name = 'TestGroup' } + } + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'Group' } -MockWith { + return @{ originId = '123'; description = 'Test Group'; name = 'TestGroup' } + } + } + + It 'should return unchanged status if properties match' { + $result = Get-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Group' + $result.status | Should -Be 'Unchanged' + $result.Ensure | Should -Be 'Present' + } + + It 'should return changed status if properties differ' { + $result = Get-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'New Description' + $result.status | Should -Be 'Changed' + $result.propertiesChanged | Should -Contain 'Description' + } + } + + Context 'When group is renamed' { + BeforeAll { + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'LiveGroups' } -MockWith { + return @{ originId = '123'; description = 'Test Group'; name = 'NewTestGroup' } + } + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'Group' } -MockWith { + return @{ originId = '123'; description = 'Test Group'; name = 'OldTestGroup' } + } + } + + It 'should detect renamed group' { + $result = Get-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Group' + $result.status | Should -Be 'Changed' + $result.propertiesChanged[0] | Should -Be 'Name' + } + } + + Context 'When group is missing in live cache but present in local cache' { + BeforeAll { + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'LiveGroups' } -MockWith { + return $null + } + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'Group' } -MockWith { + return @{ originId = '123'; description = 'Test Group'; name = 'TestGroup' } + } + } + + It 'should return not found status' { + $result = Get-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Group' + $result.status | Should -Be 'NotFound' + $result.propertiesChanged | Should -Be @('description', 'displayName') + } + } + + Context 'When group is present in live cache but missing in local cache' { + BeforeAll { + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'LiveGroups' } -MockWith { + return @{ originId = '123'; description = 'Test Group'; name = 'TestGroup' } + } + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'Group' } -MockWith { + return $null + } + } + + It 'should return changed' { + $result = Get-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Group' + $result.status | Should -Be 'Changed' + } + + It 'should return changed if properties differ' { + $result = Get-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'New Description' + $result.status | Should -Be 'Changed' + $result.propertiesChanged | Should -Contain 'description' + } + } + + Context 'When both live cache and local cache are missing the group' { + BeforeAll { + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'LiveGroups' } -MockWith { + return $null + } + Mock -CommandName Get-CacheItem -ParameterFilter { $Type -eq 'Group' } -MockWith { + return $null + } + } + + It 'should return not found status' { + $result = Get-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Group' + $result.status | Should -Be 'NotFound' + $result.propertiesChanged | Should -Be @('description', 'displayName') + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/New-AzDoOrganizationGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/New-AzDoOrganizationGroup.tests.ps1 new file mode 100644 index 000000000..41aaa3d0f --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/New-AzDoOrganizationGroup.tests.ps1 @@ -0,0 +1,76 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'New-AzDoOrganizationGroup' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-AzDoOrganizationGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mock the external functions used within New-AzDoOrganizationGroup + Mock -CommandName New-DevOpsGroup -MockWith { + return @{ + principalName = "testPrincipalName" + } + } + + Mock -CommandName Refresh-CacheIdentity + Mock -CommandName Add-CacheItem + Mock -CommandName Set-CacheObject + + } + + Context 'When GroupName is provided' { + It 'Should create a new DevOps group with correct parameters' { + $params = @{ + GroupName = 'TestGroup' + GroupDescription = 'Test Description' + } + + $result = New-AzDoOrganizationGroup @params + + Assert-MockCalled -CommandName New-DevOpsGroup -Exactly -Times 1 + Assert-MockCalled -CommandName Refresh-CacheIdentity -Exactly -Times 1 + Assert-MockCalled -CommandName Add-CacheItem -Exactly -Times 1 + Assert-MockCalled -CommandName Set-CacheObject -Exactly -Times 1 + } + } + + Context 'Verbose logs' { + It 'Should log verbose messages' { + + Mock -CommandName Write-Verbose + + $params = @{ + GroupName = 'TestGroup' + GroupDescription = 'Test Description' + Verbose = $true + } + + $verboseOutput = New-AzDoOrganizationGroup @params + + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { $Message -like "*Creating a new DevOps group with GroupName: 'TestGroup', GroupDescription: 'Test Description'*" } + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { $Message -like "*Updated global AzDoGroup cache object.*" } + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Remove-AzDoOrganizationGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Remove-AzDoOrganizationGroup.tests.ps1 new file mode 100644 index 000000000..48d5cd741 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Remove-AzDoOrganizationGroup.tests.ps1 @@ -0,0 +1,127 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-AzDoOrganizationGroup' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + Remove-Variable -Name AZDOLiveGroups -Scope Global + Remove-Variable -Name AzDoGroup -Scope Global + } + + BeforeAll { + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoOrganizationGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mock the external functions used within Remove-AzDoOrganizationGroup + Mock -CommandName Remove-DevOpsGroup + Mock -CommandName Remove-CacheItem + Mock -CommandName Set-CacheObject + + } + + BeforeEach { + # Reset global variables before each test + $Global:DSCAZDO_OrganizationName = "TestOrg" + $Global:AZDOLiveGroups = @{} + $Global:AzDoGroup = @{} + } + + Context 'When no cache items exist' { + It 'Should return without performing any operations' { + $lookupResult = @{ + liveCache = $null + localCache = $null + } + + Remove-AzDoOrganizationGroup -GroupName 'TestGroup' -LookupResult $lookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Times 0 + Assert-MockCalled -CommandName Remove-CacheItem -Times 0 + Assert-MockCalled -CommandName Set-CacheObject -Times 0 + } + } + + Context 'When group is found in live cache' { + It 'Should remove the group and update the caches' { + $lookupResult = @{ + liveCache = @{ + Descriptor = 'LiveDescriptor' + principalName = 'livePrincipalName' + } + localCache = $null + } + + Remove-AzDoOrganizationGroup -GroupName 'TestGroup' -LookupResult $lookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Exactly -Times 1 -ParameterFilter { + $GroupDescriptor -eq 'LiveDescriptor' -and + $ApiUri -eq 'https://vssps.dev.azure.com/TestOrg' + } + + Assert-MockCalled -CommandName Remove-CacheItem -Exactly -Times 2 + Assert-MockCalled -CommandName Set-CacheObject -Exactly -Times 2 + } + } + + Context 'When group is found in local cache but not in live cache' { + It 'Should remove the group and update the caches' { + $lookupResult = @{ + liveCache = $null + localCache = @{ + Descriptor = 'LocalDescriptor' + principalName = 'localPrincipalName' + } + } + + Remove-AzDoOrganizationGroup -GroupName 'TestGroup' -LookupResult $lookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Exactly -Times 1 -ParameterFilter { + $GroupDescriptor -eq 'LocalDescriptor' -and + $ApiUri -eq 'https://vssps.dev.azure.com/TestOrg' + } + + Assert-MockCalled -CommandName Remove-CacheItem -Exactly -Times 2 + Assert-MockCalled -CommandName Set-CacheObject -Exactly -Times 2 + } + } + + Context 'When both live and local cache are present' { + It 'Should prioritize live cache and remove the group' { + $lookupResult = @{ + liveCache = @{ + Descriptor = 'LiveDescriptor' + principalName = 'livePrincipalName' + } + localCache = @{ + Descriptor = 'LocalDescriptor' + principalName = 'localPrincipalName' + } + } + + Remove-AzDoOrganizationGroup -GroupName 'TestGroup' -LookupResult $lookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Exactly -Times 1 -ParameterFilter { + $GroupDescriptor -eq 'LiveDescriptor' -and + $ApiUri -eq 'https://vssps.dev.azure.com/TestOrg' + } + + Assert-MockCalled -CommandName Remove-CacheItem -Exactly -Times 2 + Assert-MockCalled -CommandName Set-CacheObject -Exactly -Times 2 + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Set-AzDoOrganizationGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Set-AzDoOrganizationGroup.tests.ps1 new file mode 100644 index 000000000..c2a16d68c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Set-AzDoOrganizationGroup.tests.ps1 @@ -0,0 +1,175 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Set-AzDoOrganizationGroup' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + Remove-Variable -Name AzDoGroup -Scope Global + } + + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoOrganizationGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mock the external functions used within Set-AzDoOrganizationGroup + Mock -CommandName Set-DevOpsGroup -MockWith { + return @{ + principalName = 'testPrincipalName' + descriptor = 'testDescriptor' + } + } + + Mock -CommandName Refresh-CacheIdentity + Mock -CommandName Remove-CacheItem + Mock -CommandName Add-CacheItem + Mock -CommandName Set-CacheObject + Mock -CommandName Write-Warning + + } + BeforeEach { + # Reset global variables before each test + $Global:DSCAZDO_OrganizationName = "TestOrg" + $Global:AzDoGroup = @{} + } + + Context 'When group has been renamed' { + It 'Should write a warning and return without setting the group' { + $lookupResult = @{ + Status = [DSCGetSummaryState]::Renamed + liveCache = @{ + descriptor = 'liveDescriptor' + } + localCache = @{ + principalName = 'localPrincipalName' + } + } + + $result = Set-AzDoOrganizationGroup -GroupName 'TestGroup' -LookupResult $lookupResult + + $result | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Set-DevOpsGroup -Times 0 -Scope It + Assert-MockCalled -CommandName Refresh-CacheIdentity -Times 0 -Scope It + Assert-MockCalled -CommandName Remove-CacheItem -Times 0 -Scope It + Assert-MockCalled -CommandName Add-CacheItem -Times 0 -Scope It + Assert-MockCalled -CommandName Set-CacheObject -Times 0 -Scope It + } + } + + Context 'When group needs to be set' { + It 'Should set the group and update the caches' { + $lookupResult = @{ + Status = [DSCGetSummaryState]::None + liveCache = @{ + descriptor = 'liveDescriptor' + } + localCache = @{ + principalName = 'localPrincipalName' + } + } + + $result = Set-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Description' -LookupResult $lookupResult + + $result | Should -Not -BeNullOrEmpty + $result.principalName | Should -Be 'testPrincipalName' + + Assert-MockCalled -CommandName Set-DevOpsGroup -Exactly -Times 1 -ParameterFilter { + $ApiUri -eq 'https://vssps.dev.azure.com/TestOrg' -and + $GroupName -eq 'TestGroup' -and + $GroupDescription -eq 'Test Description' -and + $GroupDescriptor -eq 'liveDescriptor' + } + + Assert-MockCalled -CommandName Refresh-CacheIdentity -Exactly -Times 1 -ParameterFilter { + $Identity.principalName -eq 'testPrincipalName' -and + $CacheType -eq 'LiveGroups' + } + + Assert-MockCalled -CommandName Remove-CacheItem -Exactly -Times 1 -ParameterFilter { + $Key -eq 'localPrincipalName' -and + $Type -eq 'Group' + } + + Assert-MockCalled -CommandName Add-CacheItem -Exactly -Times 1 -ParameterFilter { + $Key -eq 'testPrincipalName' -and + $Type -eq 'Group' + } + + Assert-MockCalled -CommandName Set-CacheObject -Exactly -Times 1 -ParameterFilter { + $CacheType -eq 'Group' + } + } + } + + Context 'When there is no local cache' { + It 'Should set the group and update the caches without removing any cache item' { + $lookupResult = @{ + Status = [DSCGetSummaryState]::None + liveCache = @{ + descriptor = 'liveDescriptor' + } + localCache = $null + } + + $result = Set-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Description' -LookupResult $lookupResult + + $result | Should -Not -BeNullOrEmpty + $result.principalName | Should -Be 'testPrincipalName' + + Assert-MockCalled -CommandName Set-DevOpsGroup -Exactly -Times 1 -ParameterFilter { + $ApiUri -eq 'https://vssps.dev.azure.com/TestOrg' -and + $GroupName -eq 'TestGroup' -and + $GroupDescription -eq 'Test Description' -and + $GroupDescriptor -eq 'liveDescriptor' + } + + Assert-MockCalled -CommandName Refresh-CacheIdentity -Exactly -Times 1 -ParameterFilter { + $Identity.principalName -eq 'testPrincipalName' -and + $CacheType -eq 'LiveGroups' + } + + Assert-MockCalled -CommandName Remove-CacheItem -Times 0 -Scope It + Assert-MockCalled -CommandName Add-CacheItem -Exactly -Times 1 -ParameterFilter { + $Key -eq 'testPrincipalName' -and + $Type -eq 'Group' + } + + Assert-MockCalled -CommandName Set-CacheObject -Exactly -Times 1 -ParameterFilter { + $CacheType -eq 'Group' + } + } + } + + Context 'When an exception occurs while setting the group' { + + It 'Should throw the exception' { + + Mock -CommandName Set-DevOpsGroup -MockWith { + throw 'An error occurred' + } + + $lookupResult = @{ + Status = [DSCGetSummaryState]::None + liveCache = @{ + descriptor = 'liveDescriptor' + } + localCache = $null + } + + { Set-AzDoOrganizationGroup -GroupName 'TestGroup' -GroupDescription 'Test Description' -LookupResult $lookupResult } | Should -Throw 'An error occurred' + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Test-AzDoOrganizationGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Test-AzDoOrganizationGroup.tests.ps1 new file mode 100644 index 000000000..18c597264 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationGroup/Test-AzDoOrganizationGroup.tests.ps1 @@ -0,0 +1,77 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Pester tests +# Not required to run in the pipeline +Describe "Test-AzDoOrganizationGroup" -skip { + + BeforeAll { + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDoOrganizationGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Mock the external functions used within Test-AzDoOrganizationGroup + Mock -CommandName Test-AzDoOrganizationGroup + + } + + Context "when the group exists" { + It "should return true" { + # Mock Test-AzDoOrganizationGroup function to simulate group existence + Mock -CommandName Test-AzDoOrganizationGroup -MockWith { + param ( + [string]$GroupName, + [string]$Pat, + [string]$ApiUri + ) + return $true + } + + $result = Test-AzDoOrganizationGroup -GroupName 'ExistingGroup' -Pat 'dummyPat' -ApiUri 'https://dev.azure.com/myorg' + $result | Should -Be $true + } + } + + Context "when the group does not exist" { + It "should return false" { + # Mock Test-AzDoOrganizationGroup function to simulate group non-existence + Mock -CommandName Test-AzDoOrganizationGroup -MockWith { + param ( + [string]$GroupName, + [string]$Pat, + [string]$ApiUri + ) + return $false + } + + $result = Test-AzDoOrganizationGroup -GroupName 'NonExistentGroup' -Pat 'dummyPat' -ApiUri 'https://dev.azure.com/myorg' + $result | Should -Be $false + } + } + + Context "when there is an empty GroupName parameter" { + It "should throw an error" { + { Test-AzDoOrganizationGroup -GroupName '' -Pat 'dummyPat' -ApiUri 'https://dev.azure.com/myorg' } | Should -Throw + } + } + + Context "when there is an empty Pat parameter" { + It "should throw an error" { + { Test-AzDoOrganizationGroup -GroupName 'ExistingGroup' -Pat '' -ApiUri 'https://dev.azure.com/myorg' } | Should -Throw + } + } + + Context "when there is an empty ApiUri parameter" { + It "should throw an error" { + { Test-AzDoOrganizationGroup -GroupName 'ExistingGroup' -Pat 'dummyPat' -ApiUri '' } | Should -Throw + } + } + +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Get-AzDoProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Get-AzDoProject.tests.ps1 new file mode 100644 index 000000000..b344b71e0 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Get-AzDoProject.tests.ps1 @@ -0,0 +1,117 @@ +$currentFile = $MyInvocation.MyCommand.Path +# Pester tests for Get-AzDoProject + +Describe "Get-AzDoProject" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Define common mock responses + $mockProject = @{ + ProjectName = 'ExistingProject' + description = 'ExistingDescription' + SourceControlType = 'Git' + Visibility = 'Private' + } + + $mockProcessTemplate = @{ + ProcessTemplate = 'Agile' + } + + Mock -CommandName Test-AzDevOpsProjectName -MockWith { return $true } + Mock -CommandName Write-Warning + + } + + Context "when the project exists" { + BeforeEach { + # Mock Get-CacheItem to return an existing project + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'ExistingProject' -and $Type -eq 'LiveProjects' } -MockWith { return $mockProject } + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'Agile' -and $Type -eq 'LiveProcesses' } -MockWith { return $mockProcessTemplate } + } + + It "should return the project details with status unchanged" { + $result = Get-AzDoProject -ProjectName 'ExistingProject' -ProjectDescription 'ExistingDescription' -SourceControlType 'Git' -ProcessTemplate 'Agile' -Visibility 'Private' + $result.Status | Should -Be 'Unchanged' + $result.ProjectName | Should -Be 'ExistingProject' + $result.ProjectDescription | Should -Be 'ExistingDescription' + } + + It "should return status changed when descriptions differ" { + $result = Get-AzDoProject -ProjectName 'ExistingProject' -ProjectDescription 'NewDescription' -SourceControlType 'Git' -ProcessTemplate 'Agile' -Visibility 'Private' + $result.Status | Should -Be 'Changed' + $result.propertiesChanged | Should -Contain 'Description' + } + + It "should return status changed when visibility differs" { + $result = Get-AzDoProject -ProjectName 'ExistingProject' -ProjectDescription 'ExistingDescription' -SourceControlType 'Git' -ProcessTemplate 'Agile' -Visibility 'Public' + $result.Status | Should -Be 'Changed' + $result.propertiesChanged | Should -Contain 'Visibility' + } + } + + Context "when the project does not exist" { + BeforeEach { + # Mock Get-CacheItem to return null for non-existing project + Mock -CommandName Get-CacheItem -ParameterFilter { $true } -MockWith { return $null } + } + + It "should return status NotFound" { + $result = Get-AzDoProject -ProjectName 'NonExistentProject' -ProjectDescription 'AnyDescription' -SourceControlType 'Git' -ProcessTemplate 'Agile' -Visibility 'Private' + $result.Status | Should -Be 'NotFound' + } + } + + Context "when the process template does not exist" { + BeforeEach { + # Mock Get-CacheItem to return null for non-existing process template + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'ExistingProject' -and $Type -eq 'LiveProjects' } -MockWith { return $mockProject } + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'NonExistentTemplate' -and $Type -eq 'LiveProcesses' } -MockWith { return $null } + } + + It "should throw an error" { + { Get-AzDoProject -ProjectName 'ExistingProject' -ProjectDescription 'ExistingDescription' -SourceControlType 'Git' -ProcessTemplate 'NonExistentTemplate' -Visibility 'Private' } | Should -Throw + } + } + + Context "when source control type differs" { + BeforeEach { + # Mock Get-CacheItem to return an existing project with different source control type + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'ExistingProject' -and $Type -eq 'LiveProjects' } -MockWith { + $mockProject.SourceControlType = 'Tfvc' + return $mockProject + } + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'Agile' -and $Type -eq 'LiveProcesses' } -MockWith { return $mockProcessTemplate } + } + + It "should warn about source control type conflict" { + $result = Get-AzDoProject -ProjectName 'ExistingProject' -ProjectDescription 'ExistingDescription' -SourceControlType 'Git' -ProcessTemplate 'Agile' -Visibility 'Private' + $result.Status | Should -Be 'UnChanged' + $result.ProjectName | Should -Be 'ExistingProject' + $result.SourceControlType | Should -Be 'Git' + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/New-AzDoProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/New-AzDoProject.tests.ps1 new file mode 100644 index 000000000..202f0670c --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/New-AzDoProject.tests.ps1 @@ -0,0 +1,144 @@ +$currentFile = $MyInvocation.MyCommand.Path +# Pester tests for New-AzDoProject + +Describe "New-AzDoProject" { + + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-AzDoProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Define common mock responses + $mockProcessTemplate = @{ + id = '12345' + ProcessTemplate = 'Agile' + } + + $mockProjectJob = @{ + url = 'https://dev.azure.com/TestOrg/_apis/projects/ExistingProject' + } + + Mock -CommandName Test-AzDevOpsProjectName -MockWith { return $true } + Mock -CommandName Refresh-AzDoCache + + } + + Context "when parameters are valid" { + BeforeEach { + # Mock Get-CacheItem to return a process template + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'Agile' -and $Type -eq 'LiveProcesses' } -MockWith { return $mockProcessTemplate } + + # Mock New-DevOpsProject to simulate project creation + Mock -CommandName New-DevOpsProject -MockWith { return $mockProjectJob } + + # Mock Wait-DevOpsProject to simulate waiting for project creation + Mock -CommandName Wait-DevOpsProject + + # Mock Refresh-AzDoCache to simulate cache refresh + Mock -CommandName Refresh-AzDoCache + + } + + It "should create a new project with specified parameters" { + New-AzDoProject -ProjectName 'NewProject' -ProjectDescription 'New Project Description' -SourceControlType 'Git' -ProcessTemplate 'Agile' -Visibility 'Private' + + # Validate that Get-CacheItem was called with correct parameters + Assert-MockCalled -CommandName Get-CacheItem -Exactly 1 -ParameterFilter { $Key -eq 'Agile' -and $Type -eq 'LiveProcesses' } + + # Validate that New-DevOpsProject was called with correct parameters + Assert-MockCalled -CommandName New-DevOpsProject -Exactly 1 -ParameterFilter { + ($organization -eq 'TestOrganization') -and + ($projectName -eq 'NewProject') -and + ($description -eq 'New Project Description') -and + ($sourceControlType -eq 'Git') -and + ($processTemplateId -eq '12345') -and + ($visibility -eq 'Private') + } + + # Validate that Wait-DevOpsProject was called with correct parameters + Assert-MockCalled -CommandName Wait-DevOpsProject -Exactly 1 -ParameterFilter { + $ProjectURL -eq 'https://dev.azure.com/TestOrg/_apis/projects/ExistingProject' -and + $OrganizationName -eq 'TestOrganization' + } + + # Validate that Refresh-AzDoCache was called with correct parameters + Assert-MockCalled -CommandName Refresh-AzDoCache -Exactly 1 -ParameterFilter { + $OrganizationName -eq 'TestOrganization' + } + } + } + + Context "when process template does not exist" { + BeforeEach { + # Mock Get-CacheItem to return null for non-existing process template + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'NonExistentTemplate' -and $Type -eq 'LiveProcesses' } -MockWith { return $null } + } + + It "should throw an error if process template is not found" { + { New-AzDoProject -ProjectName 'NewProject' -ProjectDescription 'New Project Description' -SourceControlType 'Git' -ProcessTemplate 'NonExistentTemplate' -Visibility 'Private' } | Should -Throw + } + } + + Context "when force parameter is used" -skip { + BeforeEach { + # Mock Get-CacheItem to return a process template + Mock -CommandName Get-CacheItem -ParameterFilter { $Key -eq 'Agile' -and $Type -eq 'LiveProcesses' } -MockWith { return $mockProcessTemplate } + + # Mock New-DevOpsProject to simulate project creation + Mock -CommandName New-DevOpsProject -MockWith { return $mockProjectJob } + + # Mock Wait-DevOpsProject to simulate waiting for project creation + Mock -CommandName Wait-DevOpsProject + + # Mock Refresh-AzDoCache to simulate cache refresh + Mock -CommandName Refresh-AzDoCache + } + + It "should create a new project even if it already exists when -Force is used" { + New-AzDoProject -ProjectName 'NewProject' -ProjectDescription 'New Project Description' -SourceControlType 'Git' -ProcessTemplate 'Agile' -Visibility 'Private' -Force + + # Validate that New-DevOpsProject was called with correct parameters + Assert-MockCalled -CommandName New-DevOpsProject -Exactly 1 -ParameterFilter { + $organization -eq 'TestOrg' -and + $projectName -eq 'NewProject' -and + $description -eq 'New Project Description' -and + $sourceControlType -eq 'Git' -and + $processTemplateId -eq '12345' -and + $visibility -eq 'Private' + } + + # Validate that Wait-DevOpsProject was called with correct parameters + Assert-MockCalled -CommandName Wait-DevOpsProject -Exactly 1 -ParameterFilter { + $ProjectURL -eq 'https://dev.azure.com/TestOrg/_apis/projects/ExistingProject' -and + $OrganizationName -eq 'TestOrg' + } + + # Validate that Refresh-AzDoCache was called with correct parameters + Assert-MockCalled -CommandName Refresh-AzDoCache -Exactly 1 -ParameterFilter { + $OrganizationName -eq 'TestOrg' + } + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Remove-AzDoProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Remove-AzDoProject.tests.ps1 new file mode 100644 index 000000000..ca73fda14 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Remove-AzDoProject.tests.ps1 @@ -0,0 +1,97 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Remove-AzDoProject" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + Mock -CommandName Test-AzDevOpsProjectName -MockWith { return $true } + Mock -CommandName Get-CacheItem -MockWith { return @{ id = '12345' } } + Mock -CommandName Remove-DevOpsProject + Mock -CommandName Remove-CacheItem + Mock -CommandName Export-CacheObject + + } + + Context "When the project exists in cache" { + + It "Should remove the project from Azure DevOps and update the cache" { + # Arrange + $Global:DSCAZDO_OrganizationName = "TestOrg" + $projectName = "TestProject" + + # Act + Remove-AzDoProject -ProjectName $projectName + + # Assert + Assert-MockCalled -CommandName Get-CacheItem -Exactly -Times 1 -ParameterFilter { + ($Key -eq $projectName) -and + ($Type -eq 'LiveProjects') + } + + Assert-MockCalled -CommandName Remove-DevOpsProject -Exactly -Times 1 -ParameterFilter { + ($Organization -eq "TestOrg") -and + ($ProjectId -eq '12345') + } + + Assert-MockCalled -CommandName Remove-CacheItem -Exactly -Times 1 -ParameterFilter { + ($Key -eq $projectName) -and + ($Type -eq 'LiveProjects') + } + + Assert-MockCalled -CommandName Export-CacheObject -Exactly -Times 1 -ParameterFilter { + ($CacheType -eq 'LiveProjects') -and + ($Content -eq $AzDoLiveProjects) + } + + } + } + + Context "When the project does not exist in cache" { + + It "Should not attempt to remove the project or update the cache" { + + Mock -CommandName Get-CacheItem + + # Arrange + $Global:DSCAZDO_OrganizationName = "TestOrg" + $projectName = "NonExistentProject" + + # Act + Remove-AzDoProject -ProjectName $projectName + + # Assert + Assert-MockCalled -CommandName Get-CacheItem -Exactly -Times 1 -ParameterFilter { + $Key -eq $projectName + $Type -eq 'LiveProjects' + } + Assert-MockCalled -CommandName Remove-DevOpsProject -Exactly -Times 0 + Assert-MockCalled -CommandName Remove-CacheItem -Exactly -Times 0 + Assert-MockCalled -CommandName Export-CacheObject -Exactly -Times 0 + + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Set-AzDoProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Set-AzDoProject.tests.ps1 new file mode 100644 index 000000000..bed82e44a --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Set-AzDoProject.tests.ps1 @@ -0,0 +1,94 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Set-AzDoProject" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + Mock -CommandName Test-AzDevOpsProjectName -MockWith { + return $true + } + + Mock -CommandName Get-CacheItem -MockWith { + param ($Key, $Type) + if ($Type -eq 'LiveProjects') + { + return @{ id = '12345' } + } + elseif ($Type -eq 'LiveProcesses') + { + return @{ id = '67890' } + } + } + + Mock -CommandName Update-DevOpsProject -MockWith { + return @{ url = "http://devopsprojecturl" } + } + + Mock -CommandName Wait-DevOpsProject + Mock -CommandName Refresh-AzDoCache + + } + + Context "When setting a project" { + + It "Should update the project in Azure DevOps and refresh the cache" { + # Arrange + $Global:DSCAZDO_OrganizationName = "TestOrg" + $projectName = "TestProject" + $projectDescription = "Test Description" + $sourceControlType = "Git" + $processTemplate = "Agile" + $visibility = "Private" + + # Act + Set-AzDoProject -ProjectName $projectName -ProjectDescription $projectDescription -SourceControlType $sourceControlType -ProcessTemplate $processTemplate -Visibility $visibility + + # Assert + Assert-MockCalled -CommandName Get-CacheItem -Exactly -Times 1 -ParameterFilter { + ($Key -eq $projectName) -and + ($Type -eq 'LiveProjects') + } + Assert-MockCalled -CommandName Get-CacheItem -Exactly -Times 1 -ParameterFilter { + ($Key -eq $processTemplate) -and + ($Type -eq 'LiveProcesses') + } + Assert-MockCalled -CommandName Update-DevOpsProject -Exactly -Times 1 -ParameterFilter { + ($organization -eq "TestOrg") -and + ($projectId -eq '12345') -and + ($description -eq $projectDescription) -and + ($processTemplateId -eq '67890') + } + Assert-MockCalled -CommandName Wait-DevOpsProject -Exactly -Times 1 -ParameterFilter { + ($ProjectURL -eq "http://devopsprojecturl") -and + ($OrganizationName -eq "TestOrg") + } + Assert-MockCalled -CommandName Refresh-AzDoCache -Exactly -Times 1 -ParameterFilter { + $OrganizationName -eq "TestOrg" + } + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Test-AzDoProject.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Test-AzDoProject.tests.ps1 new file mode 100644 index 000000000..fc7b98aaa --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProject/Test-AzDoProject.tests.ps1 @@ -0,0 +1,78 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "Test-AzDevOpsProject" -Skip { + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDoProject.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + $fakeUri = "https://dev.azure.com/fakeOrganization/_apis/" + $fakePat = "fakePat" + $fakeProjectId = "fakeProjectId" + $fakeProjectName = "fakeProjectName" + + Mock -CommandName Test-AzDevOpsApiUri -MockWith { + return $true + } + + Mock -CommandName Test-AzDevOpsPat -MockWith { + return $true + } + + Mock -CommandName Test-AzDevOpsProjectId -MockWith { + return $true + } + + Mock -CommandName Test-AzDevOpsProjectName -MockWith { + return $true + } + + Mock -CommandName Get-AzDevOpsProject -MockWith { + return @{ id = $fakeProjectId } + } + + } + + It "Should return true when project exists by ProjectId" { + $result = Test-AzDevOpsProject -ApiUri $fakeUri -Pat $fakePat -ProjectId $fakeProjectId + $result | Should -Be $true + } + + It "Should return true when project exists by ProjectName" { + $result = Test-AzDevOpsProject -ApiUri $fakeUri -Pat $fakePat -ProjectName $fakeProjectName + $result | Should -Be $true + } + + It "Should return true when project exists by ProjectId and ProjectName" { + $result = Test-AzDevOpsProject -ApiUri $fakeUri -Pat $fakePat -ProjectId $fakeProjectId -ProjectName $fakeProjectName + $result | Should -Be $true + } + + It "Should return false when project does not exist" { + Mock -CommandName Get-AzDevOpsProject -MockWith { + param ($ApiUri, $Pat, $ProjectId, $ProjectName) + return $null + } + + $result = Test-AzDevOpsProject -ApiUri $fakeUri -Pat $fakePat -ProjectId $fakeProjectId + $result | Should -Be $false + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Get-AzDoProjectGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Get-AzDoProjectGroup.tests.ps1 new file mode 100644 index 000000000..5d221bc9d --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Get-AzDoProjectGroup.tests.ps1 @@ -0,0 +1,148 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Get-AzDoProjectGroup' { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Get-AzDoProjectGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + $mockProjectName = "TestProject" + $mockGroupName = "TestGroup" + $mockDescription = "TestDescription" + + Mock -CommandName Test-AzDevOpsProjectName -MockWith { return $true } + Mock -CommandName Get-CacheItem -MockWith { + switch ($Type) { + 'LiveProjects' { return @{ originId = 1 } } + 'LiveGroups' { return @{ originId = 1 } } + 'Group' { return @{ originId = 1 } } + } + } + Mock -CommandName Remove-CacheItem + Mock -CommandName Add-CacheItem + Mock -CommandName Format-AzDoGroup -MockWith { + return ('{0}:{1}' -f $mockProjectName, $mockGroupName) + } + + } + + It 'should call Get-CacheItem for livegroup lookup' { + Get-AzDoProjectGroup -ProjectName $mockProjectName -GroupName $mockGroupName + Assert-MockCalled Get-CacheItem -ParameterFilter { + $Key -eq "$mockProjectName" -and $Type -eq 'LiveProjects' + } -Times 1 + } + + It 'should return correct status when livegroup and localgroup originId differ' { + + Mock -CommandName Get-CacheItem -MockWith { return @{ originId = 1 } } -ParameterFilter { + $Type -eq 'LiveGroups' + } + Mock -CommandName Get-CacheItem -MockWith { return @{ originId = 2 } } -ParameterFilter { + $Type -eq 'Group' + } + Mock -CommandName Find-CacheItem -MockWith { return @{ originId = 1 } } + + Mock -CommandName Format-AzDoGroup -MockWith { + return ('{0}:{1}' -f $mockProjectName, $mockGroupName) + } + + $result = Get-AzDoProjectGroup -ProjectName $mockProjectName -GroupName $mockGroupName + + $result.status | Should -Be 'Renamed' + } + + It 'should return status NotFound when livegroup is absent but localgroup is present' { + Mock -CommandName Get-CacheItem -MockWith { + param ($Key, $Type) + if ($Type -eq 'LiveGroups') { return $null } + if ($Type -eq 'Group') { return @{originId = 1} } + } + + Mock -CommandName Format-AzDoGroup -MockWith { + return ('{0}:{1}' -f $mockProjectName, $mockGroupName) + } + + $result = Get-AzDoProjectGroup -ProjectName $mockProjectName -GroupName $mockGroupName + + $result.status | Should -Be 'NotFound' + } + + It 'should return status Changed when properties differ' { + Mock -CommandName Get-CacheItem -MockWith { + return @{description = 'OldDescription'; name = $mockGroupName; originId = 1} + } + Mock -CommandName Format-AzDoGroup -MockWith { + return ('{0}:{1}' -f $mockProjectName, $mockGroupName) + } + + $result = Get-AzDoProjectGroup -ProjectName $mockProjectName -GroupName $mockGroupName -GroupDescription $mockDescription + + $result.status | Should -Be 'Changed' + $result.propertiesChanged | Should -Contain 'description' + } + + It 'should return status Unchanged when properties are same' { + Mock -CommandName Get-CacheItem -MockWith { + return @{description = $mockDescription; name = $mockGroupName; originId = 1} + } + Mock -CommandName Format-AzDoGroup -MockWith { + return ('{0}:{1}' -f $mockProjectName, $mockGroupName) + } + + $result = Get-AzDoProjectGroup -ProjectName $mockProjectName -GroupName $mockGroupName -GroupDescription $mockDescription + + $result.status | Should -Be 'Unchanged' + } + + It 'should add current livegroup to cache if localgroup was not present' { + + Mock -CommandName Get-CacheItem -MockWith { + if ($Type -eq 'LiveGroups') { + return @{ + description = $mockDescription + displayName = $mockGroupName + originId = 1 + } + } + elseif ($Type -eq 'Group') { + return $null + } + else { + return $null + } + } + + Mock -CommandName Format-AzDoGroup -MockWith { + return ('{0}:{1}' -f $mockProjectName, $mockGroupName) + } + + Get-AzDoProjectGroup -ProjectName $mockProjectName -GroupName $mockGroupName -GroupDescription $mockDescription + + Assert-MockCalled -CommandName Add-CacheItem -ParameterFilter { + ($Key -eq ('{0}:{1}' -f $mockProjectName, $mockGroupName)) -and ($Type -eq 'Group') + } -Times 1 + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/New-AzDoProjectGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/New-AzDoProjectGroup.tests.ps1 new file mode 100644 index 000000000..3f5f81f61 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/New-AzDoProjectGroup.tests.ps1 @@ -0,0 +1,106 @@ +# Save this script as New-AzDoProjectGroup.Tests.ps1 + +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'New-AzDoProjectGroup' { + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-AzDoProjectGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mocking external dependencies + Mock -CommandName Write-Verbose + Mock -CommandName Write-Warning + Mock -CommandName New-DevOpsGroup -MockWith { + return [PSCustomObject]@{ principalName = "TestPrincipal" } + } + Mock -CommandName Get-CacheItem + Mock -CommandName Add-CacheItem + Mock -CommandName Set-CacheObject + Mock -CommandName Refresh-CacheIdentity + + } + + BeforeEach { + $Global:DSCAZDO_OrganizationName = 'TestOrg' + $Global:AzDoGroup = @() + } + + Context 'when ProjectScopeDescriptor is found' { + BeforeEach { + Mock -CommandName Get-CacheItem -MockWith { + return [PSCustomObject]@{ ProjectDescriptor = 'ProjectDescriptor123' } + } + } + + It 'should create a new DevOps group' { + $params = @{ + GroupName = 'TestGroup' + ProjectName = 'TestProject' + } + + $result = New-AzDoProjectGroup @params + + Assert-MockCalled Get-CacheItem -Exactly 1 + Assert-MockCalled New-DevOpsGroup -Exactly 1 + Assert-MockCalled Add-CacheItem -Exactly 1 + Assert-MockCalled Set-CacheObject -Exactly 1 + Assert-MockCalled Refresh-CacheIdentity -Exactly 1 + } + + It 'should update caches correctly' { + $params = @{ + GroupName = 'TestGroup' + ProjectName = 'TestProject' + } + + $result = New-AzDoProjectGroup @params + + Assert-MockCalled Add-CacheItem -Exactly 1 + Assert-MockCalled Set-CacheObject -Exactly 1 + Assert-MockCalled Refresh-CacheIdentity -Exactly 1 + } + } + + Context 'when ProjectScopeDescriptor is not found' { + BeforeEach { + Mock -CommandName Get-CacheItem -MockWith { + return $null + } + } + + It 'should write a warning and abort the group creation' { + $params = @{ + GroupName = 'TestGroup' + ProjectName = 'TestProject' + } + + $result = New-AzDoProjectGroup @params + + Assert-MockCalled Get-CacheItem -Exactly 1 + Assert-MockCalled Write-Warning -Exactly 1 + Assert-MockCalled New-DevOpsGroup -Exactly 0 + Assert-MockCalled Add-CacheItem -Exactly 0 + Assert-MockCalled Set-CacheObject -Exactly 0 + Assert-MockCalled Refresh-CacheIdentity -Exactly 0 + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Remove-AzDoProjectGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Remove-AzDoProjectGroup.tests.ps1 new file mode 100644 index 000000000..4b54e4836 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Remove-AzDoProjectGroup.tests.ps1 @@ -0,0 +1,121 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe 'Remove-AzDoProjectGroup' { + + AfterAll { + # Clean up + Remove-Variable -Name DSCAZDO_OrganizationName -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Remove-AzDoProjectGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + $mockProjectName = "TestProject" + $mockGroupName = "TestGroup" + $mockDescription = "TestDescription" + + # Mocking external functions that are called within the function + Mock -CommandName Remove-DevOpsGroup -Verifiable + Mock -CommandName Remove-CacheItem -Verifiable + Mock -CommandName Set-CacheObject -Verifiable + + } + + Context 'When LookupResult has no cache items' { + It 'Should return without calling any other functions' { + $LookupResult = @{ + liveCache = $null + localCache = $null + } + + $result = Remove-AzDoProjectGroup -GroupName 'TestGroup' -ProjectName 'TestProject' -LookupResult $LookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Times 0 -Exactly + Assert-MockCalled -CommandName Remove-CacheItem -Times 0 -Exactly + Assert-MockCalled -CommandName Set-CacheObject -Times 0 -Exactly + } + } + + Context 'When LookupResult has liveCache but no localCache' { + + It 'Should call Remove-DevOpsGroup and remove cache items' { + $LookupResult = @{ + liveCache = @{ + Descriptor = 'liveDescriptor' + principalName = 'livePrincipal' + } + localCache = $null + } + + $result = Remove-AzDoProjectGroup -GroupName 'TestGroup' -ProjectName 'TestProject' -LookupResult $LookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Times 1 -Exactly + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -ParameterFilter { $type -eq 'LiveGroups' } + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -Exactly -ParameterFilter { $cacheType -eq 'LiveGroups' } + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -Exactly -ParameterFilter { $type -eq 'Group' } + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -Exactly -ParameterFilter { $cacheType -eq 'Group' } + } + } + + Context 'When LookupResult has both liveCache and localCache' { + It 'Should use liveCache descriptor and principal name' { + $LookupResult = @{ + liveCache = @{ + Descriptor = 'liveDescriptor' + principalName = 'livePrincipal' + } + localCache = @{ + Descriptor = 'localDescriptor' + principalName = 'localPrincipal' + } + } + + $result = Remove-AzDoProjectGroup -GroupName 'TestGroup' -ProjectName 'TestProject' -LookupResult $LookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Times 1 + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -ParameterFilter { $type -eq 'LiveGroups' } + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -ParameterFilter { $cacheType -eq 'LiveGroups' } + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -ParameterFilter { $type -eq 'Group' } + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -ParameterFilter { $cacheType -eq 'Group' } + } + } + + Context 'When only localCache exists' { + It 'Should use localCache descriptor and principal name' { + $LookupResult = @{ + liveCache = $null + localCache = @{ + Descriptor = 'localDescriptor' + principalName = 'localPrincipal' + } + } + + $result = Remove-AzDoProjectGroup -GroupName 'TestGroup' -ProjectName 'TestProject' -LookupResult $LookupResult + + Assert-MockCalled -CommandName Remove-DevOpsGroup -Times 1 + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -ParameterFilter { $type -eq 'LiveGroups' } + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -ParameterFilter { $cacheType -eq 'LiveGroups' } + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -ParameterFilter { $type -eq 'Group' } + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -ParameterFilter { $cacheType -eq 'Group' } + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Set-AzDoProjectGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Set-AzDoProjectGroup.tests.ps1 new file mode 100644 index 000000000..043693b01 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Set-AzDoProjectGroup.tests.ps1 @@ -0,0 +1,129 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Import the module containing Set-AzDoProjectGroup if it's in a different file. +# . .\path\to\your\module.psm1 + +Describe 'Set-AzDoProjectGroup' { + + AfterAll { + # Clean up + Remove-Variable -Name DSCAZDO_OrganizationName -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Set-AzDoProjectGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + + # Mocking external functions that are called within the function + Mock -CommandName Set-DevOpsGroup -MockWith { + return @{ principalName = 'newPrincipal'; descriptor = 'newDescriptor'; } + } + + Mock -CommandName Refresh-CacheIdentity + Mock -CommandName Remove-CacheItem + Mock -CommandName Add-CacheItem + Mock -CommandName Set-CacheObject + Mock -CommandName Write-Warning + + } + + Context 'When LookupResult status is Renamed' { + It 'Should write a warning and return without making any API calls' { + $LookupResult = @{ + Status = [DSCGetSummaryState]::Renamed + liveCache = @{ + descriptor = 'liveDescriptor' + } + } + + $result = Set-AzDoProjectGroup -GroupName 'TestGroup' -ProjectName 'TestProject' -LookupResult $LookupResult + + Assert-MockCalled -CommandName Set-DevOpsGroup -Times 0 -Exactly + Assert-MockCalled -CommandName Refresh-CacheIdentity -Times 0 -Exactly + Assert-MockCalled -CommandName Remove-CacheItem -Times 0 -Exactly + Assert-MockCalled -CommandName Add-CacheItem -Times 0 -Exactly + Assert-MockCalled -CommandName Set-CacheObject -Times 0 -Exactly + } + } + + Context 'When updating the group' { + It 'Should call Set-DevOpsGroup with correct parameters and update caches' { + $LookupResult = @{ + Status = [DSCGetSummaryState]::Existing + liveCache = @{ + descriptor = 'liveDescriptor' + } + localCache = @{ + principalName = 'localPrincipal' + } + } + + $Global:DSCAZDO_OrganizationName = 'TestOrg' + + $result = Set-AzDoProjectGroup -GroupName 'TestGroup' -GroupDescription 'TestDescription' -ProjectName 'TestProject' -LookupResult $LookupResult + + Assert-MockCalled -CommandName Set-DevOpsGroup -Times 1 -Exactly -ParameterFilter { + ($ApiUri -eq "https://vssps.dev.azure.com/TestOrg") -and + ($GroupName -eq 'TestGroup') -and + ($GroupDescription -eq 'TestDescription') -and + ($GroupDescriptor -eq 'liveDescriptor') + } + + Assert-MockCalled -CommandName Refresh-CacheIdentity -Times 1 -Exactly -ParameterFilter { + $key -eq 'newPrincipal' -and + $cacheType -eq 'LiveGroups' + } + + Assert-MockCalled -CommandName Remove-CacheItem -Times 1 -Exactly -ParameterFilter { + $key -eq 'localPrincipal' -and + $type -eq 'Group' + } + + Assert-MockCalled -CommandName Add-CacheItem -Times 1 -Exactly -ParameterFilter { + $key -eq 'newPrincipal' -and + $type -eq 'Group' + } + + Assert-MockCalled -CommandName Set-CacheObject -Times 1 -Exactly -ParameterFilter { + $cacheType -eq 'Group' + } + } + } + + Context 'When LookupResult has no local cache' { + It 'Should not call Remove-CacheItem' { + $LookupResult = @{ + Status = [DSCGetSummaryState]::Existing + liveCache = @{ + descriptor = 'liveDescriptor' + } + localCache = $null + } + + $Global:DSCAZDO_OrganizationName = 'TestOrg' + + $result = Set-AzDoProjectGroup -GroupName 'TestGroup' -GroupDescription 'TestDescription' -ProjectName 'TestProject' -LookupResult $LookupResult + + Assert-MockCalled -CommandName Remove-CacheItem -Times 0 -Exactly + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Test-AzDoProjectGroup.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Test-AzDoProjectGroup.tests.ps1 new file mode 100644 index 000000000..84c8f2d79 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoProjectGroup/Test-AzDoProjectGroup.tests.ps1 @@ -0,0 +1,124 @@ +$currentFile = $MyInvocation.MyCommand.Path + +# Not used +Describe 'Test-AzDoProjectGroup' -skip { + + AfterAll { + # Clean up + Remove-Variable -Name DSCAZDO_OrganizationName -ErrorAction SilentlyContinue + } + + BeforeAll { + + # Set the organization name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'Test-AzDoProjectGroup.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Load the summary state + . (Get-ClassFilePath 'DSCGetSummaryState') + . (Get-ClassFilePath '000.CacheItem') + . (Get-ClassFilePath 'Ensure') + + # Mocking external functions that are called within the function + Mock -CommandName 'Get-CacheItem' -MockWith { + param ($Key, $Type) + if ($Key -eq "groupKey" -and $Type -eq 'LiveGroups') { + return $true + } + return $false + } + Mock -CommandName 'Format-AzDoGroup' -MockWith { return "groupKey" } + } + + Context 'When parameters are valid' { + It 'Should return true when group is found in cache' { + $GroupName = 'TestGroup' + $GetResult = @{ + Status = [DSCGetSummaryState]::Unchanged + Current = @{ description = 'Group Description' } + } + + $result = Test-AzDoProjectGroup -GroupName $GroupName -GetResult $GetResult + $result | Should -BeTrue + } + + It 'Should return false when group name and description matches' { + $GroupName = 'TestGroup' + $GroupDescription = 'Group Description' + $GetResult = @{ + Status = [DSCGetSummaryState]::Unchanged + Current = @{ description = $GroupDescription } + } + + $result = Test-AzDoProjectGroup -GroupName $GroupName -GroupDescription $GroupDescription -GetResult $GetResult + $result | Should -BeFalse + } + + It 'Should return true when status is Changed and group present in both live and cache' { + $GroupName = 'TestGroup' + $GetResult = @{ + Status = [DSCGetSummaryState]::Changed + Current = @{} + Cache = @{} + } + + $result = Test-AzDoProjectGroup -GroupName $GroupName -GetResult $GetResult + $result | Should -BeTrue + } + + It 'Should return true when status is Changed and group present in live but not cache' { + $GroupName = 'TestGroup' + $GetResult = @{ + Status = [DSCGetSummaryState]::Changed + Current = @{} + Cache = $null + } + + $result = Test-AzDoProjectGroup -GroupName $GroupName -GetResult $GetResult + $result | Should -BeTrue + } + + It 'Should return true when status is Changed and group not present in live but in cache' { + $GroupName = 'TestGroup' + $GetResult = @{ + Status = [DSCGetSummaryState]::Changed + Current = $null + Cache = @{} + } + + $result = Test-AzDoProjectGroup -GroupName $GroupName -GetResult $GetResult + $result | Should -BeTrue + } + + It 'Should return false when status is Renamed' { + $GroupName = 'TestGroup' + $GetResult = @{ + Status = [DSCGetSummaryState]::Renamed + } + + $result = Test-AzDoProjectGroup -GroupName $GroupName -GetResult $GetResult + $result | Should -BeFalse + } + + It 'Should return true when group present in cache' { + $GroupName = 'TestGroup' + $GetResult = @{ + Status = [DSCGetSummaryState]::Missing + } + + $result = Test-AzDoProjectGroup -GroupName $GroupName -GetResult $GetResult + $result | Should -BeTrue + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDoAuthenticationProvider.tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDoAuthenticationProvider.tests.ps1 new file mode 100644 index 000000000..70a7a4127 --- /dev/null +++ b/tests/Unit/Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/New-AzDoAuthenticationProvider.tests.ps1 @@ -0,0 +1,130 @@ +$currentFile = $MyInvocation.MyCommand.Path + +Describe "New-AzDoAuthenticationProvider" { + + AfterAll { + Remove-Variable -Name DSCAZDO_OrganizationName -Scope Global + } + + BeforeAll { + + # Set the Organization Name + $Global:DSCAZDO_OrganizationName = 'TestOrganization' + + # Load the functions to test + if ($null -eq $currentFile) { + $currentFile = Join-Path -Path $PSScriptRoot -ChildPath 'New-AzDoAuthenticationProvider.tests.ps1' + } + + # Load the functions to test + $files = Get-FunctionItem (Find-MockedFunctions -TestFilePath $currentFile) + + ForEach ($file in $files) { + . $file.FullName + } + + # Mocking dependencies + Mock -CommandName Set-AzPersonalAccessToken -MockWith { return "mockedToken" } + Mock -CommandName Get-AzManagedIdentityToken -MockWith { return "mockedManagedIdentityToken" } + Mock -CommandName Get-AzDoCacheObjects -MockWith { return @() } + Mock -CommandName Get-Command + Mock -CommandName Initialize-CacheObject + Mock -CommandName Export-Clixml + + } + + BeforeEach { + $ENV:AZDODSC_CACHE_DIRECTORY = "C:\MockCacheDirectory" + $Global:DSCAZDO_AuthenticationToken = $null + } + + AfterEach { + $ENV:AZDODSC_CACHE_DIRECTORY = $null + $Global:DSCAZDO_AuthenticationToken = $null + } + + Context "When AZDODSC_CACHE_DIRECTORY is not set" { + + It "Should throw an error" { + # Arrange + $ENV:AZDODSC_CACHE_DIRECTORY = $null + + # Act & Assert + { + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -PersonalAccessToken "dummyPat" + } | Should -Throw "*The Environment Variable 'AZDODSC_CACHE_DIRECTORY' is not set*" + } + } + + Context "Using PersonalAccessToken parameter set" { + It "Should set the global authentication token without verification" { + # Act + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -PersonalAccessToken "dummyPat" -NoVerify + + # Assert + $Global:DSCAZDO_AuthenticationToken | Should -Be "mockedToken" + Assert-MockCalled -CommandName Set-AzPersonalAccessToken -Exactly 1 + } + + It "Should set the global authentication token with verification" { + # Act + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -PersonalAccessToken "dummyPat" + + # Assert + $Global:DSCAZDO_AuthenticationToken | Should -Be "mockedToken" + Assert-MockCalled -CommandName Set-AzPersonalAccessToken -Exactly 1 -ParameterFilter { $Verify } + } + } + + Context "Using ManagedIdentity parameter set" { + It "Should set the global authentication token without verification" { + # Act + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -useManagedIdentity -NoVerify + + # Assert + $Global:DSCAZDO_AuthenticationToken | Should -Be "mockedManagedIdentityToken" + Assert-MockCalled -CommandName Get-AzManagedIdentityToken -Exactly 1 + } + + It "Should set the global authentication token with verification" { + # Act + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -useManagedIdentity + + # Assert + $Global:DSCAZDO_AuthenticationToken | Should -Be "mockedManagedIdentityToken" + Assert-MockCalled -CommandName Get-AzManagedIdentityToken -Exactly 1 -ParameterFilter { $Verify } + } + } + + Context "Using SecureStringPersonalAccessToken parameter set" { + It "Should set the global authentication token" { + # Arrange + $secureStringPAT = ConvertTo-SecureString "dummySecurePat" -AsPlainText -Force + + # Act + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -SecureStringPersonalAccessToken $secureStringPAT + + # Assert + $Global:DSCAZDO_AuthenticationToken | Should -Be "mockedToken" + Assert-MockCalled -CommandName Set-AzPersonalAccessToken -Exactly 1 + } + } + + Context "Token export functionality" { + It "Should export token information when isResource is not set" { + # Act + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -PersonalAccessToken "dummyPat" + + # Assert + Assert-MockCalled -CommandName Export-Clixml -Exactly 1 + } + + It "Should not export token information when isResource is set" { + # Act + New-AzDoAuthenticationProvider -OrganizationName "Contoso" -PersonalAccessToken "dummyPat" -isResource + + # Assert + Assert-MockCalled -CommandName Export-Clixml -Exactly 0 + } + } +} diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesApiUri.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesApiUri.Tests.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesApiUri.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesApiUri.Tests.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesUri.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesUri.Tests.disabled similarity index 100% rename from tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesUri.Tests.ps1 rename to tests/Unit/Modules/AzureDevOpsDsc.Common/Services/Functions/Public/Get-AzDevOpsServicesUri.Tests.disabled diff --git a/tests/Unit/Modules/AzureDevOpsDsc/AzureDevOpsDsc.DscClassResources.Tests.ps1 b/tests/Unit/Modules/AzureDevOpsDsc/AzureDevOpsDsc.DscClassResources.Tests.ps1 deleted file mode 100644 index 3a298f6de..000000000 --- a/tests/Unit/Modules/AzureDevOpsDsc/AzureDevOpsDsc.DscClassResources.Tests.ps1 +++ /dev/null @@ -1,30 +0,0 @@ - -# # Initialize tests -# . $PSScriptRoot\AzureDevOpsDsc.TestInitialization.ps1 - - -# InModuleScope 'AzureDevOpsDsc' { - -# Describe 'DSCClassResources\AzDevOpsApiDscResourceBase' -Tag 'AzDevOpsApiDscResourceBase' { - -# $dscModuleName = 'AzureDevOpsDsc' -# $testCasesValidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Valid' -# $testCasesValidResourceNamesForDscResources = $testCasesValidResourceNames | Where-Object { $_.ResourceName -notin @('Operation')} - -# Context "When evaluating '$dscModuleName' module" { -# BeforeAll { -# $dscModuleName = 'AzureDevOpsDsc' -# $dscResourcePrefix = 'AzDevOps' -# [string[]]$exportedDscResources = (Get-Module $dscModuleName).ExportedDscResources -# } - -# It "Should contain an exported, DSCResource specific to the 'ResourceName' - ''" -TestCases $testCasesValidResourceNamesForDscResources { -# param ([string]$ResourceName) - -# "$dscResourcePrefix$ResourceName" | Should -BeIn $exportedDscResources -# } - -# } - -# } -# } diff --git a/tests/Unit/Modules/TestHelpers/CommonTestFunctions.psm1 b/tests/Unit/Modules/TestHelpers/CommonTestFunctions.psm1 new file mode 100644 index 000000000..206eca773 --- /dev/null +++ b/tests/Unit/Modules/TestHelpers/CommonTestFunctions.psm1 @@ -0,0 +1,109 @@ +Function Split-RecurivePath { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Path, + [Parameter(Mandatory = $false)] + [int]$Times = 1 + ) + + 1 .. $Times | ForEach-Object { + $Path = Split-Path -Path $Path -Parent + } + + $Path +} + +Function Get-FunctionItem { + param( + [string[]]$FileNames + ) + + # Locate the scriptroot for the module + if ($Global:RepositoryRoot -eq $null) { + $Global:RepositoryRoot = Split-RecurivePath $PSScriptRoot -Times 4 + } + + $ScriptRoot = $Global:RepositoryRoot + + if ($null -eq $Global:TestPaths) { + $Global:TestPaths = Get-ChildItem -LiteralPath $ScriptRoot -Recurse -File -Include *.ps1 | Where-Object { + ($_.FullName -notlike "*Tests.ps1") -and + ($_.FullName -notlike '*\output\*') -and + ($_.FullName -notlike '*\tests\*') + } + } + + # Perform a lookup for all BeforeEach FileNames + $BeforeEachPath = @() + ForEach ($FileName in $FileNames) { + $BeforeEachPath += $Global:TestPaths | Where-Object { $_.Name -eq $FileName } + } + + return $BeforeEachPath + +} + +Function Find-MockedFunctions { + param( + [String]$TestFilePath + ) + + $files = @() + + # + # Using the File path of the test file, work out the function that is being tested + $FunctionName = (Get-Item -LiteralPath $TestFilePath).BaseName -replace '\.tests$', '' + $files += "$($FunctionName).ps1" + + + # + # Load the function into the AST and look for the mock commands. + + # Parse the PowerShell script file + $AST = [System.Management.Automation.Language.Parser]::ParseFile($TestFilePath, [ref]$null, [ref]$null) + + # Find all the Mock commands + $MockCommands = $AST.FindAll({ + $args[0] -is [System.Management.Automation.Language.CommandAst] -and + $args[0].CommandElements[0].Value -eq 'Mock' + }, $true) + + # Iterate over the Mock commands and find the CommandName parameter + foreach ($mockCommand in $MockCommands) { + + # Iterate over the CommandElements + foreach ($element in $mockCommand.CommandElements) { + + # Check if the element is a CommandParameterAst and the parameter name is CommandName + if ($element -is [System.Management.Automation.Language.CommandParameterAst] -and $element.ParameterName -eq 'CommandName') { + $null = $element.Parent.Extent.Text -match '(-CommandName\s+(?[^\s]+))|(^Mock (?[^\s]+$))' + $files += "$($matches.Function).ps1" + } + } + } + + # Ignore the following list of functions + $files = $files | Where-Object { $_ -notin @('Write-Error.ps1', 'Write-Output.ps1', 'Write-Verbose.ps1', 'Write-Warning.ps1') } + # Return the unique list of functions + $files = $files | Select-Object -Unique + + $files + +} + +Function Get-ClassFilePath { + param( + [string]$FileName + ) + + $Class = $Global:TestPaths | Where-Object { ($_.Name -eq $FileName) -or ($_.Name -eq "$FileName.ps1") } + return $Class.FullName + +} + +Function Import-Enums { + return ($Global:TestPaths | Where-Object { $_.Directory.Name -eq 'Enum' }) +} + +Export-ModuleMember -Function Split-RecurivePath, Get-FunctionItem, Find-MockedFunctions, Get-ClassFilePath, Import-Enums diff --git a/tests/Unit/Modules/TestHelpers/CommonTestHelper.psm1 b/tests/Unit/Modules/TestHelpers/CommonTestHelper.psm1 index e809c4c45..eaf0a0735 100644 --- a/tests/Unit/Modules/TestHelpers/CommonTestHelper.psm1 +++ b/tests/Unit/Modules/TestHelpers/CommonTestHelper.psm1 @@ -249,3 +249,22 @@ function Get-CommandParameterSetParameter Where-Object { $_.Name -like $ParameterSetName}).Parameters } + + +function Set-OutputDirAsModulePath +{ + [CmdletBinding()] + param ( + [Parameter()] + [String] + $RepositoryRoot + ) + + # Set the module path if it is not already set + if ($ENV:PSModulePath -like "*$($RepositoryRoot)*") { return } + + $ModulePath = '{0}{1}\' -f (($IsLinux) ? ':' : ';'), $RepositoryRoot + $ENV:PSModulePath = '{0}{1}\output' -f $ENV:PSModulePath, $ModulePath + $ENV:PSModulePath = '{0}{1}\output\AzureDevOpsDsc\0.0.0\Modules' -f $ENV:PSModulePath, $ModulePath + +} diff --git a/tests/Unit/Modules/_USEFUL.AzureDevOpsDsc.Common.Resources.Functions.Tests.Old.ps1 b/tests/Unit/Modules/_USEFUL.AzureDevOpsDsc.Common.Resources.Functions.Tests.Old.ps1 deleted file mode 100644 index 5da51c672..000000000 --- a/tests/Unit/Modules/_USEFUL.AzureDevOpsDsc.Common.Resources.Functions.Tests.Old.ps1 +++ /dev/null @@ -1,161 +0,0 @@ - -# Initialize tests -. $PSScriptRoot\AzureDevOpsDsc.Common.TestInitialization.ps1 - - -InModuleScope 'AzureDevOpsDsc.Common' { - - Describe 'Resources\Functions' { - - $moduleName = 'AzureDevOpsDsc.Common' - $testCasesValidResourceNames = Get-TestCase -ScopeName 'ResourceName' -TestCaseName 'Valid' - $testCasesValidResourceNamesForDscResources = $testCasesValidResourceNames | Where-Object { $_.ResourceName -notin @('Operation')} - - Context "When evaluating 'AzureDevOpsDsc.Common' module functions" { - BeforeEach { - $moduleName = 'AzureDevOpsDsc.Common' - } - - Context "When evaluating public, 'ExportedFunctions'" { - - BeforeEach { - [string[]]$exportedFunctionNames = Get-Command -Module $moduleName - - $resourcesFunctionsPublicDirectoryPath = "$PSScriptRoot\..\..\..\..\source\Modules\$moduleName\Resources\Functions\Public" - $resourcesFunctionsPublicTestsDirectoryPath = "$PSScriptRoot\Resources\Functions\Public" - } - - - $testCasesValidResourcePublicFunctionNames = Get-TestCase -ScopeName 'ResourcePublicFunctionName' -TestCaseName 'Valid' - $testCasesValidDscResourcePublicFunctionNames = Get-TestCase -ScopeName 'DscResourcePublicFunctionName' -TestCaseName 'Valid' - - $testCasesValidApiResourcePublicFunctionRequiredParameterNames = Get-TestCase -ScopeName 'ApiResourcePublicFunctionRequiredParameterName' -TestCaseName 'Valid' - - $testCasesValidDscResourcePublicFunctionRequiredParameterNames = Join-TestCaseArray -Expand -TestCaseArray @( - $testCasesValidDscResourcePublicFunctionNames, - $testCasesValidApiResourcePublicFunctionRequiredParameterNames - ) - - $testCasesValidApiResourcePublicFunctionMandatoryParameterNames = Get-TestCase -ScopeName 'ApiResourcePublicFunctionMandatoryParameterName' -TestCaseName 'Valid' - - $testCasesValidDscResourcePublicFunctionMandatoryParameterNames = Join-TestCaseArray -Expand -TestCaseArray @( - $testCasesValidDscResourcePublicFunctionNames, - $testCasesValidApiResourcePublicFunctionMandatoryParameterNames - ) - - $testCasesValidParameterAliasNames = Get-TestCase -ScopeName 'ParameterAliasName' -TestCaseName 'Valid' - - - - Context "When evaluating all public, functions" { - - - # Note: $testCasesExportedFunctionNames contains all exported functions in the module - - #It "Does not return a null value when 'Get-Command' is called - ''" -TestCases $testCasesExportedFunctionNames { - # param ([string]$ExportedFunctionName) - # - # Get-Command "$ExportedFunctionName" | Should -Not -BeNullOrEmpty - #} - - It "When evaluating function parameter, aliases required for DSC Resource functions" { - # TODO - } - - } - - - - Context "When evaluating all public, functions required for DSC Resources" { - - It "Should contain an exported, '' function (specific to the 'ResourceName') - ''" -TestCases $testCasesValidDscResourcePublicFunctionNames { - param ([string]$DscResourcePublicFunctionName) - - $DscResourcePublicFunctionName | Should -BeIn $exportedFunctionNames - } - - It "Should return a '' function/command (specific to the 'ResourceName') from 'Get-Command' - ''" -TestCases $testCasesValidDscResourcePublicFunctionNames { - param ([string]$DscResourcePublicFunctionName) - - Get-Command -Module $moduleName -Name $DscResourcePublicFunctionName | Should -Not -BeNullOrEmpty - } - - It "Should have a '' script ('.ps1') file (specific to the 'ResourceName') - ''" -TestCases $testCasesValidDscResourcePublicFunctionNames { - param ([string]$DscResourcePublicFunctionName) - - $functionScriptPath = Join-Path $resourcesFunctionsPublicDirectoryPath -ChildPath $($DscResourcePublicFunctionName + ".ps1") - Test-Path $functionScriptPath | Should -BeTrue - } - - It "Should have a '' test fixture/script ('.Tests.ps1') file (specific to the 'ResourceName') - ''" -TestCases $testCasesValidDscResourcePublicFunctionNames { - param ([string]$DscResourcePublicFunctionName) - - $functionTestsScriptPath = Join-Path $resourcesFunctionsPublicTestsDirectoryPath -ChildPath $($DscResourcePublicFunctionName + ".Tests.ps1") - Test-Path $functionTestsScriptPath | Should -BeTrue - } - - Context "When evaluating function parameters required for DSC Resource functions" { - - It "Should have a '' function with required, '' parameter - '', ''" -TestCases $testCasesValidDscResourcePublicFunctionRequiredParameterNames { - param ([string]$DscResourcePublicFunctionName, - [string]$ApiResourcePublicFunctionRequiredParameterName) - - $ApiResourcePublicFunctionRequiredParameterName | - Should -BeIn $((Get-CommandParameter -CommandName $DscResourcePublicFunctionName -ModuleName $moduleName).Name) - } - - Context "When evaluating function parameters required for DSC Resource functions that must be 'Mandatory'" { - - It "Should have a '' function with required (and 'Mandatory'), '' parameter - '', ''" -TestCases $testCasesValidDscResourcePublicFunctionMandatoryParameterNames { - param ([string]$DscResourcePublicFunctionName, - [string]$ApiResourcePublicFunctionMandatoryParameterName) - - $ApiResourcePublicFunctionMandatoryParameterName | - Should -BeIn $(((Get-CommandParameterSetParameter -CommandName $DscResourcePublicFunctionName -ModuleName $moduleName) | Where-Object { $_.IsMandatory -eq 1 }).Name) - } - } - } - } - - Context "When evaluating all public, functions required for non-DSC Resources" { - - It "Should contain an exported, '' function (specific to the 'ResourceName') - ''" -TestCases $testCasesValidResourcePublicFunctionNames { - param ([string]$ResourcePublicFunctionName) - - $ResourcePublicFunctionName | Should -BeIn $exportedFunctionNames - } - - It "Should return a '' function/command (specific to the 'ResourceName') from 'Get-Command' - ''" -TestCases $testCasesValidResourcePublicFunctionNames { - param ([string]$ResourcePublicFunctionName) - - Get-Command -Module $moduleName -Name $ResourcePublicFunctionName | Should -Not -BeNullOrEmpty - } - - It "Should have a '' script ('.ps1') file (specific to the 'ResourceName') - ''" -TestCases $testCasesValidResourcePublicFunctionNames { - param ([string]$ResourcePublicFunctionName) - - $functionScriptPath = Join-Path $resourcesFunctionsPublicDirectoryPath -ChildPath $($ResourcePublicFunctionName + ".ps1") - Test-Path $functionScriptPath | Should -BeTrue - } - - It "Should have a '' test fixture/script ('.Tests.ps1') file (specific to the 'ResourceName') - ''" -TestCases $testCasesValidResourcePublicFunctionNames { - param ([string]$ResourcePublicFunctionName) - - $functionTestsScriptPath = Join-Path $resourcesFunctionsPublicTestsDirectoryPath -ChildPath $($ResourcePublicFunctionName + ".Tests.ps1") - Test-Path $functionTestsScriptPath | Should -BeTrue - } - - } - - } - - Context "When evaluating private, module functions" { - - # TODO: - # Should be a 'Test-Id' function present - - } - } - - } -}