diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c1b69..8054d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added WikiSource documentation and DscResource.DocGenerator build and publish workflows. - Added post-deployment notification validation that fails the build when PowerShell Universal reports a deployment error. - Added module-scoped compound tasks for pulling a packaged module from a PSU resource repository or packaging and pushing a complete offline repository, including post-deployment validation. +- Added `UniversalSkipCertificateCheck` build task and yaml configuration setting to bypass TLS certificate validation when calling the PowerShell Universal server, useful for self-signed certificates. ### Changed - Converted the generated sample module into an InvokeBuild task module. +- Changed the offline automation repository manifest file name from a fixed `repository.psd1` to `.psd1`, and preserved the prerelease tag on the module version, so PowerShell Universal shows the correct module name and full version during deployment. ### Deprecated diff --git a/source/Private/Invoke-UniversalRestMethod.ps1 b/source/Private/Invoke-UniversalRestMethod.ps1 new file mode 100644 index 0000000..a2f4311 --- /dev/null +++ b/source/Private/Invoke-UniversalRestMethod.ps1 @@ -0,0 +1,58 @@ +function Invoke-UniversalRestMethod +{ + <# + .SYNOPSIS + Invokes Invoke-RestMethod with optional certificate validation bypass. + + .DESCRIPTION + Forwards the supplied parameters to Invoke-RestMethod. When + SkipCertificateCheck is enabled, the request bypasses TLS + certificate validation using the native parameter on PowerShell 6 + and above, or a temporary ServicePointManager callback on Windows + PowerShell. + + .PARAMETER RestMethodParameters + Parameters forwarded to Invoke-RestMethod. + + .PARAMETER SkipCertificateCheck + Whether to bypass TLS certificate validation for the request. + + .EXAMPLE + Invoke-UniversalRestMethod -RestMethodParameters $requestParameters -SkipCertificateCheck $true + #> + [CmdletBinding()] + [OutputType([System.Object])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $RestMethodParameters, + + [Parameter()] + [System.Boolean] + $SkipCertificateCheck = $false + ) + + if (-not $SkipCertificateCheck) + { + return Invoke-RestMethod @RestMethodParameters + } + + if ($PSVersionTable.PSVersion.Major -ge 6) + { + return Invoke-RestMethod @RestMethodParameters -SkipCertificateCheck + } + + $originalCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback + + try + { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } + + Invoke-RestMethod @RestMethodParameters + } + finally + { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $originalCallback + } +} diff --git a/source/Private/New-UniversalAutomationRepositoryManifest.ps1 b/source/Private/New-UniversalAutomationRepositoryManifest.ps1 index 79fed4d..e7c7d1b 100644 --- a/source/Private/New-UniversalAutomationRepositoryManifest.ps1 +++ b/source/Private/New-UniversalAutomationRepositoryManifest.ps1 @@ -5,11 +5,12 @@ function New-UniversalAutomationRepositoryManifest Creates the descriptor for a PowerShell Universal repository package. .DESCRIPTION - Writes repository.psd1 with the module name and metadata required by - PowerShell Universal when activating an offline automation repository. + Writes a .psd1 manifest with the module name, prerelease + tag, and metadata required by PowerShell Universal when activating an + offline automation repository. .PARAMETER Path - Destination path for repository.psd1. + Destination path for the .psd1 repository manifest. .PARAMETER Module Built module information used to populate repository metadata. @@ -37,10 +38,20 @@ function New-UniversalAutomationRepositoryManifest $ModuleVersion ) + $versionPart = $ModuleVersion -replace '-.*$', '' + $prereleasePart = if ($ModuleVersion -match '-(.+)$') + { + $Matches[1] + } + else + { + '' + } + $manifestParameters = @{ Path = $Path RootModule = $Module.Name - ModuleVersion = ($ModuleVersion -replace '-.*$', '') + ModuleVersion = $versionPart Guid = (New-Guid) Author = $Module.Author CompanyName = $Module.CompanyName @@ -52,6 +63,19 @@ function New-UniversalAutomationRepositoryManifest AliasesToExport = '*' } + if (-not [System.String]::IsNullOrWhiteSpace($prereleasePart)) + { + # Use PrivateData.PSData.Prerelease instead of the -Prerelease parameter because + # New-ModuleManifest only gained -Prerelease in PowerShell 6.0. PrivateData works + # identically on Windows PowerShell 5.1 and PowerShell 7, and is the same location + # PowerShellGet/PSResourceGet store the prerelease tag in the resulting manifest. + $manifestParameters['PrivateData'] = @{ + PSData = @{ + Prerelease = $prereleasePart + } + } + } + if ($PSCmdlet.ShouldProcess($Path, 'Create PowerShell Universal repository manifest')) { New-ModuleManifest @manifestParameters diff --git a/source/Public/Get-UniversalDeploymentError.ps1 b/source/Public/Get-UniversalDeploymentError.ps1 index e7247b4..4b4b0ce 100644 --- a/source/Public/Get-UniversalDeploymentError.ps1 +++ b/source/Public/Get-UniversalDeploymentError.ps1 @@ -20,6 +20,9 @@ function Get-UniversalDeploymentError .PARAMETER FilterText Optional text that must appear in the notification title or description. + .PARAMETER SkipCertificateCheck + Whether to bypass TLS certificate validation for the request. + .EXAMPLE Get-UniversalDeploymentError -ServerUrl $url -AppToken $token -Since $startedAt #> @@ -41,7 +44,11 @@ function Get-UniversalDeploymentError [Parameter()] [System.String] - $FilterText + $FilterText, + + [Parameter()] + [System.Boolean] + $SkipCertificateCheck = $false ) $requestParameters = @{ @@ -51,7 +58,7 @@ function Get-UniversalDeploymentError } Method = 'Get' } - $response = Invoke-RestMethod @requestParameters + $response = Invoke-UniversalRestMethod -RestMethodParameters $requestParameters -SkipCertificateCheck $SkipCertificateCheck $notifications = if ($null -ne $response.page) { @($response.page) diff --git a/source/Public/Install-UniversalModuleFromRepository.ps1 b/source/Public/Install-UniversalModuleFromRepository.ps1 index 227a79b..4a1c09f 100644 --- a/source/Public/Install-UniversalModuleFromRepository.ps1 +++ b/source/Public/Install-UniversalModuleFromRepository.ps1 @@ -29,6 +29,9 @@ function Install-UniversalModuleFromRepository .PARAMETER RepositoryAutoRemove Whether to remove the repository after the deployment attempt. + .PARAMETER SkipCertificateCheck + Whether to bypass TLS certificate validation for the request. + .EXAMPLE Install-UniversalModuleFromRepository @installParameters #> @@ -62,7 +65,11 @@ function Install-UniversalModuleFromRepository [Parameter()] [System.Boolean] - $RepositoryAutoRemove = $true + $RepositoryAutoRemove = $true, + + [Parameter()] + [System.Boolean] + $SkipCertificateCheck = $false ) $headers = @{ @@ -70,7 +77,12 @@ function Install-UniversalModuleFromRepository Accept = 'application/json' } $repositoryEndpoint = '{0}/api/v1/resourceRepository' -f $ServerUrl - $repositories = @(Invoke-RestMethod -Uri $repositoryEndpoint -Headers $headers -Method Get) + $listRepositoriesParameters = @{ + Uri = $repositoryEndpoint + Headers = $headers + Method = 'Get' + } + $repositories = @(Invoke-UniversalRestMethod -RestMethodParameters $listRepositoriesParameters -SkipCertificateCheck $SkipCertificateCheck) $existingRepository = $repositories | Where-Object -FilterScript { $_.name -eq $RepositoryName } | Select-Object -First 1 @@ -86,7 +98,12 @@ function Install-UniversalModuleFromRepository if ((-not $sameUrl -or -not [System.Boolean] $existingRepository.trusted) -and $RepositoryAutoRemove) { $deleteUri = '{0}/{1}' -f $repositoryEndpoint, [System.Uri]::EscapeDataString($RepositoryName) - $null = Invoke-RestMethod -Uri $deleteUri -Headers $headers -Method Delete + $deleteRepositoryParameters = @{ + Uri = $deleteUri + Headers = $headers + Method = 'Delete' + } + $null = Invoke-UniversalRestMethod -RestMethodParameters $deleteRepositoryParameters -SkipCertificateCheck $SkipCertificateCheck $existingRepository = $null } } @@ -100,7 +117,14 @@ function Install-UniversalModuleFromRepository id = 0 } | ConvertTo-Json -Depth 5 - $null = Invoke-RestMethod -Uri $repositoryEndpoint -Headers $headers -Method Post -Body $body -ContentType 'application/json; charset=utf-8' + $createRepositoryParameters = @{ + Uri = $repositoryEndpoint + Headers = $headers + Method = 'Post' + Body = $body + ContentType = 'application/json; charset=utf-8' + } + $null = Invoke-UniversalRestMethod -RestMethodParameters $createRepositoryParameters -SkipCertificateCheck $SkipCertificateCheck } try @@ -111,7 +135,13 @@ function Install-UniversalModuleFromRepository $ModuleVersion [System.Uri]::EscapeDataString($RepositoryName) ) - $null = Invoke-RestMethod -Uri $deployUri -Headers $headers -Method Put -ContentType 'application/octet-stream; charset=utf-8' + $deployModuleParameters = @{ + Uri = $deployUri + Headers = $headers + Method = 'Put' + ContentType = 'application/octet-stream; charset=utf-8' + } + $null = Invoke-UniversalRestMethod -RestMethodParameters $deployModuleParameters -SkipCertificateCheck $SkipCertificateCheck } finally { @@ -120,7 +150,12 @@ function Install-UniversalModuleFromRepository $deleteUri = '{0}/{1}' -f $repositoryEndpoint, [System.Uri]::EscapeDataString($RepositoryName) try { - $null = Invoke-RestMethod -Uri $deleteUri -Headers $headers -Method Delete + $deleteRepositoryParameters = @{ + Uri = $deleteUri + Headers = $headers + Method = 'Delete' + } + $null = Invoke-UniversalRestMethod -RestMethodParameters $deleteRepositoryParameters -SkipCertificateCheck $SkipCertificateCheck } catch { diff --git a/source/Public/Invoke-UniversalDeploymentUpload.ps1 b/source/Public/Invoke-UniversalDeploymentUpload.ps1 index db495de..930663c 100644 --- a/source/Public/Invoke-UniversalDeploymentUpload.ps1 +++ b/source/Public/Invoke-UniversalDeploymentUpload.ps1 @@ -17,6 +17,9 @@ function Invoke-UniversalDeploymentUpload .PARAMETER Path Path to the package that is uploaded as the request body. + .PARAMETER SkipCertificateCheck + Whether to bypass TLS certificate validation for the request. + .EXAMPLE Invoke-UniversalDeploymentUpload -Uri $uri -AppToken $token -Path $packagePath #> @@ -34,7 +37,11 @@ function Invoke-UniversalDeploymentUpload [Parameter(Mandatory = $true)] [System.String] - $Path + $Path, + + [Parameter()] + [System.Boolean] + $SkipCertificateCheck = $false ) if (-not (Test-Path -Path $Path)) @@ -52,5 +59,5 @@ function Invoke-UniversalDeploymentUpload ContentType = 'application/octet-stream; charset=utf-8' } - Invoke-RestMethod @requestParameters + Invoke-UniversalRestMethod -RestMethodParameters $requestParameters -SkipCertificateCheck $SkipCertificateCheck } diff --git a/source/Public/New-UniversalAutomationRepositoryPackage.ps1 b/source/Public/New-UniversalAutomationRepositoryPackage.ps1 index 68b806a..bdd7200 100644 --- a/source/Public/New-UniversalAutomationRepositoryPackage.ps1 +++ b/source/Public/New-UniversalAutomationRepositoryPackage.ps1 @@ -6,7 +6,8 @@ function New-UniversalAutomationRepositoryPackage .DESCRIPTION Stages the built module and its required modules using the PowerShell - Universal repository layout, creates repository.psd1, and compresses it. + Universal repository layout, creates a .psd1 repository + manifest, and compresses it. .PARAMETER BuiltModuleManifest Path to the manifest of the built project module. @@ -79,7 +80,7 @@ function New-UniversalAutomationRepositoryPackage $null = New-Item -Path $modulesDestination -ItemType Directory -Force Copy-UniversalRepositoryModule -Module $module -ModulesDestinationPath $modulesDestination -Visited @{ } - $repositoryManifestPath = Join-Path -Path $stagingDirectory -ChildPath 'repository.psd1' + $repositoryManifestPath = Join-Path -Path $stagingDirectory -ChildPath ('{0}.psd1' -f $module.Name) New-UniversalAutomationRepositoryManifest -Path $repositoryManifestPath -Module $module -ModuleVersion $ModuleVersion -Confirm:$false if (Test-Path -Path $zipPath) diff --git a/source/Public/Resolve-UniversalServerConfiguration.ps1 b/source/Public/Resolve-UniversalServerConfiguration.ps1 index 55ff95a..0c90df5 100644 --- a/source/Public/Resolve-UniversalServerConfiguration.ps1 +++ b/source/Public/Resolve-UniversalServerConfiguration.ps1 @@ -35,6 +35,12 @@ function Resolve-UniversalServerConfiguration .PARAMETER UnpinnedWasBound Indicates that Unpinned was supplied explicitly. + .PARAMETER SkipCertificateCheck + Whether to bypass TLS certificate validation for requests to the server. + + .PARAMETER SkipCertificateCheckWasBound + Indicates that SkipCertificateCheck was supplied explicitly. + .PARAMETER RequireRepository Requires and resolves resource repository settings. @@ -81,6 +87,14 @@ function Resolve-UniversalServerConfiguration [System.Boolean] $UnpinnedWasBound = $false, + [Parameter()] + [System.Boolean] + $SkipCertificateCheck = $false, + + [Parameter()] + [System.Boolean] + $SkipCertificateCheckWasBound = $false, + [Parameter()] [System.Management.Automation.SwitchParameter] $RequireRepository @@ -148,6 +162,11 @@ function Resolve-UniversalServerConfiguration $Unpinned = [System.Convert]::ToBoolean($server.UniversalUnpinned) } + if (-not $SkipCertificateCheckWasBound -and $null -ne $server.UniversalSkipCertificateCheck) + { + $SkipCertificateCheck = [System.Convert]::ToBoolean($server.UniversalSkipCertificateCheck) + } + [PSCustomObject]@{ ServerUrl = $ServerUrl.TrimEnd('/') AppToken = $AppToken @@ -155,5 +174,6 @@ function Resolve-UniversalServerConfiguration RepositoryUrl = $RepositoryUrl RepositoryAutoRemove = $RepositoryAutoRemove Unpinned = $Unpinned + SkipCertificateCheck = $SkipCertificateCheck } } diff --git a/source/Tasks/Publish.PowerShellUniversal.build.ps1 b/source/Tasks/Publish.PowerShellUniversal.build.ps1 index e31d4fe..79f032d 100644 --- a/source/Tasks/Publish.PowerShellUniversal.build.ps1 +++ b/source/Tasks/Publish.PowerShellUniversal.build.ps1 @@ -40,6 +40,10 @@ param [System.Boolean] $UniversalUnpinned = (property UniversalUnpinned $true), + [Parameter()] + [System.Boolean] + $UniversalSkipCertificateCheck = (property UniversalSkipCertificateCheck $false), + [Parameter()] [ValidateRange(0, 300)] [System.Int32] @@ -81,6 +85,8 @@ task publish_packed_module_to_universal_server { Unpinned = $UniversalUnpinned RepositoryAutoRemoveWasBound = 'UniversalPSResourceRepositoryAutoRemove' -in $TaskParameterNames UnpinnedWasBound = 'UniversalUnpinned' -in $TaskParameterNames + SkipCertificateCheck = $UniversalSkipCertificateCheck + SkipCertificateCheckWasBound = 'UniversalSkipCertificateCheck' -in $TaskParameterNames } $configuration = Sampler.PowerShellUniversalTasks\Resolve-UniversalServerConfiguration @configurationParameters $packagePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.{1}.nupkg' -f $ProjectName, $ModuleVersion) @@ -91,7 +97,7 @@ task publish_packed_module_to_universal_server { ) $script:UniversalDeploymentStartedAt = [System.DateTimeOffset]::UtcNow - $null = Sampler.PowerShellUniversalTasks\Invoke-UniversalDeploymentUpload -Uri $endpoint -AppToken $configuration.AppToken -Path $packagePath + $null = Sampler.PowerShellUniversalTasks\Invoke-UniversalDeploymentUpload -Uri $endpoint -AppToken $configuration.AppToken -Path $packagePath -SkipCertificateCheck $configuration.SkipCertificateCheck Write-Build -Color Green -Text ("Published module '{0}' version '{1}' to PowerShell Universal." -f $ProjectName, $ModuleVersion) } @@ -115,6 +121,8 @@ task publish_module_from_psresource_repos_to_universal_server { Unpinned = $UniversalUnpinned RepositoryAutoRemoveWasBound = 'UniversalPSResourceRepositoryAutoRemove' -in $TaskParameterNames UnpinnedWasBound = 'UniversalUnpinned' -in $TaskParameterNames + SkipCertificateCheck = $UniversalSkipCertificateCheck + SkipCertificateCheckWasBound = 'UniversalSkipCertificateCheck' -in $TaskParameterNames RequireRepository = $true } $configuration = Sampler.PowerShellUniversalTasks\Resolve-UniversalServerConfiguration @configurationParameters @@ -133,6 +141,7 @@ task publish_module_from_psresource_repos_to_universal_server { RepositoryName = $configuration.RepositoryName RepositoryUrl = $repositoryUrl RepositoryAutoRemove = $configuration.RepositoryAutoRemove + SkipCertificateCheck = $configuration.SkipCertificateCheck } $script:UniversalDeploymentStartedAt = [System.DateTimeOffset]::UtcNow @@ -182,6 +191,8 @@ task publish_universal_automation_repository_to_server { Unpinned = $UniversalUnpinned RepositoryAutoRemoveWasBound = 'UniversalPSResourceRepositoryAutoRemove' -in $TaskParameterNames UnpinnedWasBound = 'UniversalUnpinned' -in $TaskParameterNames + SkipCertificateCheck = $UniversalSkipCertificateCheck + SkipCertificateCheckWasBound = 'UniversalSkipCertificateCheck' -in $TaskParameterNames } $configuration = Sampler.PowerShellUniversalTasks\Resolve-UniversalServerConfiguration @configurationParameters $zipName = if ([System.String]::IsNullOrWhiteSpace($UniversalRepositoryZipName)) @@ -200,7 +211,7 @@ task publish_universal_automation_repository_to_server { ) $script:UniversalDeploymentStartedAt = [System.DateTimeOffset]::UtcNow - $deployment = Sampler.PowerShellUniversalTasks\Invoke-UniversalDeploymentUpload -Uri $endpoint -AppToken $configuration.AppToken -Path $packagePath + $deployment = Sampler.PowerShellUniversalTasks\Invoke-UniversalDeploymentUpload -Uri $endpoint -AppToken $configuration.AppToken -Path $packagePath -SkipCertificateCheck $configuration.SkipCertificateCheck Write-Build -Color Green -Text ("Deployed automation repository '{0}' version '{1}'." -f $deployment.name, $deployment.version) } @@ -224,6 +235,8 @@ task assert_universal_deployment_succeeded { Unpinned = $UniversalUnpinned RepositoryAutoRemoveWasBound = 'UniversalPSResourceRepositoryAutoRemove' -in $TaskParameterNames UnpinnedWasBound = 'UniversalUnpinned' -in $TaskParameterNames + SkipCertificateCheck = $UniversalSkipCertificateCheck + SkipCertificateCheckWasBound = 'UniversalSkipCertificateCheck' -in $TaskParameterNames } $configuration = Sampler.PowerShellUniversalTasks\Resolve-UniversalServerConfiguration @configurationParameters @@ -277,10 +290,11 @@ task assert_universal_deployment_succeeded { $notificationFilter = $UniversalDeploymentNotificationFilter $errorParameters = @{ - ServerUrl = $configuration.ServerUrl - AppToken = $configuration.AppToken - Since = $notificationSince - FilterText = $notificationFilter + ServerUrl = $configuration.ServerUrl + AppToken = $configuration.AppToken + Since = $notificationSince + FilterText = $notificationFilter + SkipCertificateCheck = $configuration.SkipCertificateCheck } $deploymentErrors = @(Sampler.PowerShellUniversalTasks\Get-UniversalDeploymentError @errorParameters) diff --git a/source/WikiSource/Configuration.md b/source/WikiSource/Configuration.md index 5441493..d1b418c 100644 --- a/source/WikiSource/Configuration.md +++ b/source/WikiSource/Configuration.md @@ -16,6 +16,7 @@ UniversalServer: UniversalRepositoryZipName: UniversalRepositoryAsModule: false UniversalUnpinned: true + UniversalSkipCertificateCheck: false UniversalDeploymentNotificationDelaySeconds: 2 UniversalDeploymentNotificationLookbackSeconds: 120 UniversalDeploymentNotificationFilter: @@ -32,6 +33,7 @@ UniversalServer: | `UniversalRepositoryZipName` | `..zip` | Optional explicit offline repository package name. | | `UniversalRepositoryAsModule` | `false` | Value sent to the deployment endpoint's `asModule` query parameter. | | `UniversalUnpinned` | `true` | Value sent to the deployment endpoint's `unpinned` query parameter. | +| `UniversalSkipCertificateCheck` | `false` | Bypass TLS certificate validation for requests to the PowerShell Universal server. Useful for self-signed certificates. On PowerShell 6 and above this uses the native `Invoke-RestMethod -SkipCertificateCheck` parameter; on Windows PowerShell it temporarily disables `ServicePointManager` certificate validation for the request. | | `UniversalDeploymentNotificationDelaySeconds` | `2` | Seconds to wait before querying notifications after deployment. Valid range: 0-300. | | `UniversalDeploymentNotificationLookbackSeconds` | `120` | Fallback lookback window when no deployment start time was recorded in the current workflow. Valid range: 1-3600. | | `UniversalDeploymentNotificationFilter` | Empty | Optional text that must appear in a deployment error title or description. | @@ -73,7 +75,7 @@ for `UniversalPSResourceRepositoryName`. ```text PsuRepository/ -|-- repository.psd1 +|-- MyModule.psd1 `-- Modules/ |-- MyModule/ | `-- 1.0.0/ @@ -86,8 +88,10 @@ dependencies. Dependencies are processed through an iterative queue, matching Sampler's packaging approach and avoiding nested recursive calls. The first discovery order is retained; when the same module is required again, its selection changes only if the newly resolved version is higher. -`repository.psd1` identifies the project module that PowerShell Universal -should load from the `Modules` directory. +`.psd1` identifies the project module that PowerShell Universal +should load from the `Modules` directory. It is named after the project +module rather than a fixed `repository.psd1` name so that PowerShell Universal +displays the correct module name and prerelease tag during deployment. Keep `UniversalRepositoryAsModule` set to `false` for this full repository layout. diff --git a/source/WikiSource/Task-Reference.md b/source/WikiSource/Task-Reference.md index 5567d27..30cde80 100644 --- a/source/WikiSource/Task-Reference.md +++ b/source/WikiSource/Task-Reference.md @@ -99,8 +99,8 @@ BuildWorkflow: - package_universal_automation_repository ``` -The package includes a top-level `repository.psd1` and versioned module folders -under `Modules`. +The package includes a top-level `.psd1` repository manifest and +versioned module folders under `Modules`. ## publish_universal_automation_repository_to_server @@ -221,6 +221,7 @@ variables: | `UniversalRepositoryZipName` | Offline repository zip file name. | | `UniversalRepositoryAsModule` | Deployment endpoint `asModule` value. | | `UniversalUnpinned` | Deployment endpoint `unpinned` value. | +| `UniversalSkipCertificateCheck` | Bypass TLS certificate validation on requests to the PowerShell Universal server. | | `UniversalDeploymentNotificationDelaySeconds` | Delay before querying notifications. | | `UniversalDeploymentNotificationLookbackSeconds` | Fallback lookback window when no deployment start time is recorded. | | `UniversalDeploymentNotificationFilter` | Optional title or description filter for deployment errors. | diff --git a/tests/Unit/Private/Invoke-UniversalRestMethod.Tests.ps1 b/tests/Unit/Private/Invoke-UniversalRestMethod.Tests.ps1 new file mode 100644 index 0000000..43c494d --- /dev/null +++ b/tests/Unit/Private/Invoke-UniversalRestMethod.Tests.ps1 @@ -0,0 +1,80 @@ +BeforeAll { + $script:moduleName = 'Sampler.PowerShellUniversalTasks' + + # If the module is not found, run the build task 'noop'. + if (-not (Get-Module -Name $script:moduleName -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # Re-import the module using force to get any code changes between runs. + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Module -Name $script:moduleName +} + +Describe 'Invoke-UniversalRestMethod' { + BeforeAll { + Mock -CommandName Invoke-RestMethod -MockWith { + [PSCustomObject]@{ name = 'response' } + } + } + + It 'Should invoke Invoke-RestMethod without a certificate bypass by default' { + InModuleScope -Parameters @{ RequestParameters = @{ Uri = 'https://psu.example.test' } } -ScriptBlock { + param + ( + $RequestParameters + ) + + $result = Invoke-UniversalRestMethod -RestMethodParameters $RequestParameters + + $result.name | Should -Be 'response' + } + + Should -Invoke -CommandName Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + -not $PSBoundParameters.ContainsKey('SkipCertificateCheck') + } + } + + It 'Should pass -SkipCertificateCheck to Invoke-RestMethod on PowerShell 6 and above' -Skip:($PSVersionTable.PSVersion.Major -lt 6) { + InModuleScope -Parameters @{ RequestParameters = @{ Uri = 'https://psu.example.test' } } -ScriptBlock { + param + ( + $RequestParameters + ) + + $null = Invoke-UniversalRestMethod -RestMethodParameters $RequestParameters -SkipCertificateCheck $true + } + + Should -Invoke -CommandName Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $SkipCertificateCheck -eq $true + } + } + + It 'Should temporarily bypass certificate validation on Windows PowerShell' -Skip:($PSVersionTable.PSVersion.Major -ge 6) { + InModuleScope -Parameters @{ RequestParameters = @{ Uri = 'https://psu.example.test' } } -ScriptBlock { + param + ( + $RequestParameters + ) + + $originalCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback + + $null = Invoke-UniversalRestMethod -RestMethodParameters $RequestParameters -SkipCertificateCheck $true + + [System.Net.ServicePointManager]::ServerCertificateValidationCallback | Should -Be $originalCallback + } + } +} diff --git a/tests/Unit/Private/New-UniversalAutomationRepositoryManifest.Tests.ps1 b/tests/Unit/Private/New-UniversalAutomationRepositoryManifest.Tests.ps1 index bdca154..6f5918b 100644 --- a/tests/Unit/Private/New-UniversalAutomationRepositoryManifest.Tests.ps1 +++ b/tests/Unit/Private/New-UniversalAutomationRepositoryManifest.Tests.ps1 @@ -34,10 +34,22 @@ Describe 'New-UniversalAutomationRepositoryManifest' { $module = New-Module -Name 'MyModule' -ScriptBlock { } New-UniversalAutomationRepositoryManifest -Path $ManifestPath -Module $module -ModuleVersion '1.2.3-preview1' -Confirm:$false - } -Parameters @{ ManifestPath = (Join-Path -Path $TestDrive -ChildPath 'repository.psd1') } + } -Parameters @{ ManifestPath = (Join-Path -Path $TestDrive -ChildPath 'MyModule.psd1') } Should -Invoke -CommandName New-ModuleManifest -Exactly -Times 1 -Scope It -ParameterFilter { - $RootModule -eq 'MyModule' -and $ModuleVersion -eq '1.2.3' + $RootModule -eq 'MyModule' -and $ModuleVersion -eq '1.2.3' -and $PrivateData.PSData.Prerelease -eq 'preview1' + } + } + + It 'Should create the repository descriptor without a prerelease tag when the version has none' { + InModuleScope -ScriptBlock { + $module = New-Module -Name 'MyModule' -ScriptBlock { } + + New-UniversalAutomationRepositoryManifest -Path $ManifestPath -Module $module -ModuleVersion '1.2.3' -Confirm:$false + } -Parameters @{ ManifestPath = (Join-Path -Path $TestDrive -ChildPath 'MyModule.psd1') } + + Should -Invoke -CommandName New-ModuleManifest -Exactly -Times 1 -Scope It -ParameterFilter { + $RootModule -eq 'MyModule' -and $ModuleVersion -eq '1.2.3' -and -not $PrivateData } } } diff --git a/tests/Unit/Public/Get-UniversalDeploymentError.Tests.ps1 b/tests/Unit/Public/Get-UniversalDeploymentError.Tests.ps1 index aa59387..ad3210b 100644 --- a/tests/Unit/Public/Get-UniversalDeploymentError.Tests.ps1 +++ b/tests/Unit/Public/Get-UniversalDeploymentError.Tests.ps1 @@ -117,4 +117,22 @@ Describe 'Get-UniversalDeploymentError' { $result.Count | Should -Be 1 $result[0].Description | Should -Match 'MyModule' } + + It 'Should pass -SkipCertificateCheck to Invoke-RestMethod when requested' -Skip:($PSVersionTable.PSVersion.Major -lt 6) { + Mock -CommandName Invoke-RestMethod -MockWith { + [PSCustomObject]@{ page = @() } + } + + $parameters = @{ + ServerUrl = 'https://psu.example.test' + AppToken = 'token' + Since = [System.DateTimeOffset]::UtcNow.AddMinutes(-1) + SkipCertificateCheck = $true + } + $null = @(Sampler.PowerShellUniversalTasks\Get-UniversalDeploymentError @parameters) + + Should -Invoke -CommandName Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $SkipCertificateCheck -eq $true + } + } } diff --git a/tests/Unit/Public/Install-UniversalModuleFromRepository.Tests.ps1 b/tests/Unit/Public/Install-UniversalModuleFromRepository.Tests.ps1 index 4188595..b88271e 100644 --- a/tests/Unit/Public/Install-UniversalModuleFromRepository.Tests.ps1 +++ b/tests/Unit/Public/Install-UniversalModuleFromRepository.Tests.ps1 @@ -53,4 +53,23 @@ Describe 'Install-UniversalModuleFromRepository' { Should -Invoke -CommandName Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Method -eq 'Put' } Should -Invoke -CommandName Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Method -eq 'Delete' } } + + It 'Should pass -SkipCertificateCheck to Invoke-RestMethod when requested' -Skip:($PSVersionTable.PSVersion.Major -lt 6) { + $installParameters = @{ + ServerUrl = 'https://psu.example.test' + AppToken = 'token' + ModuleName = 'MyModule' + ModuleVersion = '1.2.3' + RepositoryName = 'internal' + RepositoryUrl = 'https://packages.example.test' + RepositoryAutoRemove = $true + SkipCertificateCheck = $true + } + + Sampler.PowerShellUniversalTasks\Install-UniversalModuleFromRepository @installParameters + + Should -Invoke -CommandName Invoke-RestMethod -Scope It -ParameterFilter { + $SkipCertificateCheck -eq $true + } + } } diff --git a/tests/Unit/Public/Invoke-UniversalDeploymentUpload.Tests.ps1 b/tests/Unit/Public/Invoke-UniversalDeploymentUpload.Tests.ps1 index 64ad89a..ed8d6c1 100644 --- a/tests/Unit/Public/Invoke-UniversalDeploymentUpload.Tests.ps1 +++ b/tests/Unit/Public/Invoke-UniversalDeploymentUpload.Tests.ps1 @@ -59,4 +59,31 @@ Describe 'Invoke-UniversalDeploymentUpload' { Sampler.PowerShellUniversalTasks\Invoke-UniversalDeploymentUpload @uploadParameters } | Should -Throw } + + It 'Should not bypass certificate validation by default' { + $uploadParameters = @{ + Uri = 'https://psu.example.test/api/v1/deployment' + AppToken = 'token' + Path = (Join-Path -Path $TestDrive -ChildPath 'package.zip') + } + $null = Sampler.PowerShellUniversalTasks\Invoke-UniversalDeploymentUpload @uploadParameters + + Should -Invoke -CommandName Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + -not $PSBoundParameters.ContainsKey('SkipCertificateCheck') + } + } + + It 'Should pass -SkipCertificateCheck to Invoke-RestMethod when requested' -Skip:($PSVersionTable.PSVersion.Major -lt 6) { + $uploadParameters = @{ + Uri = 'https://psu.example.test/api/v1/deployment' + AppToken = 'token' + Path = (Join-Path -Path $TestDrive -ChildPath 'package.zip') + SkipCertificateCheck = $true + } + $null = Sampler.PowerShellUniversalTasks\Invoke-UniversalDeploymentUpload @uploadParameters + + Should -Invoke -CommandName Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $SkipCertificateCheck -eq $true + } + } } diff --git a/tests/Unit/Public/Resolve-UniversalServerConfiguration.Tests.ps1 b/tests/Unit/Public/Resolve-UniversalServerConfiguration.Tests.ps1 index 75c90cd..d92144f 100644 --- a/tests/Unit/Public/Resolve-UniversalServerConfiguration.Tests.ps1 +++ b/tests/Unit/Public/Resolve-UniversalServerConfiguration.Tests.ps1 @@ -34,6 +34,7 @@ Describe 'Resolve-UniversalServerConfiguration' { UniversalPSResourceRepositoryUrl = 'https://packages.example.test' UniversalPSResourceRepositoryAutoRemove = $false UniversalUnpinned = $false + UniversalSkipCertificateCheck = $true } } @@ -45,6 +46,37 @@ Describe 'Resolve-UniversalServerConfiguration' { $configuration.RepositoryUrl | Should -Be 'https://packages.example.test' $configuration.RepositoryAutoRemove | Should -BeFalse $configuration.Unpinned | Should -BeFalse + $configuration.SkipCertificateCheck | Should -BeTrue + } + + It 'Should default SkipCertificateCheck to $false' { + $configurationParameters = @{ + ServerUrl = 'https://psu.example.test' + AppToken = 'token' + RequireRepository = $true + } + $configuration = Sampler.PowerShellUniversalTasks\Resolve-UniversalServerConfiguration @configurationParameters + + $configuration.SkipCertificateCheck | Should -BeFalse + } + + It 'Should prefer the explicit SkipCertificateCheck parameter over the build configuration when bound' { + $buildInfo = @{ + UniversalServer = @{ + UniversalSkipCertificateCheck = $true + } + } + $configurationParameters = @{ + BuildInfo = $buildInfo + ServerUrl = 'https://psu.example.test' + AppToken = 'token' + RequireRepository = $true + SkipCertificateCheck = $false + SkipCertificateCheckWasBound = $true + } + $configuration = Sampler.PowerShellUniversalTasks\Resolve-UniversalServerConfiguration @configurationParameters + + $configuration.SkipCertificateCheck | Should -BeFalse } It 'Should use the default repository when no repository is configured' {