feat: Implement API client and token models with hashing and JWT authentication
- Added ApiClientModel and ApiTokenModel for managing API clients and tokens. - Introduced ConfigurationDefinitionModel and ConfigurationValueModel for configuration management. - Created CredentialSecretModel for storing credential secrets. - Developed DeploymentArtifactModel and DeploymentBatchModel for deployment management. - Enhanced DeploymentTargetModel and DeploymentTemplateSelectionModel to support template revisions. - Added TemplateRevisionModel and TemplateVersionModel for versioning templates. - Implemented ApiClientSecretHasher for secure secret hashing. - Created ApiTokenService for generating and validating JWT tokens. - Updated QueueJobService to handle deployment requests with artifacts. - Configured authentication settings in appsettings.json for JWT and Negotiate authentication.
This commit is contained in:
310
.tests/Api.Tests.ps1
Normal file
310
.tests/Api.Tests.ps1
Normal file
@@ -0,0 +1,310 @@
|
||||
param(
|
||||
[string]$ApiBaseUrl = 'http://localhost:5286/api',
|
||||
|
||||
[string]$DeploymentBatchId = '80000000-0000-0000-0000-000000000101',
|
||||
|
||||
[int]$ApiTimeoutSec = 30,
|
||||
|
||||
[bool]$UseDefaultCredentials = $true,
|
||||
|
||||
[System.Management.Automation.PSCredential]$Credential
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Invoke-TestMergeApiJson {
|
||||
param(
|
||||
[ValidateSet('GET', 'POST', 'PUT', 'DELETE')]
|
||||
[string]$Method,
|
||||
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/'))
|
||||
$parameters = @{
|
||||
Method = $Method
|
||||
Uri = $uri
|
||||
TimeoutSec = $ApiTimeoutSec
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
|
||||
if ($Credential) {
|
||||
$parameters.Credential = $Credential
|
||||
}
|
||||
elseif ($UseDefaultCredentials) {
|
||||
$parameters.UseDefaultCredentials = $true
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-RestMethod @parameters
|
||||
}
|
||||
catch {
|
||||
$responseBody = '<empty response body>'
|
||||
if ($_.Exception.Response) {
|
||||
try {
|
||||
$stream = $_.Exception.Response.GetResponseStream()
|
||||
if ($stream) {
|
||||
$reader = [System.IO.StreamReader]::new($stream)
|
||||
$text = $reader.ReadToEnd()
|
||||
if (-not [string]::IsNullOrWhiteSpace($text)) {
|
||||
$responseBody = $text
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$responseBody = '<could not read response body>'
|
||||
}
|
||||
}
|
||||
|
||||
throw "API request failed. Method=[$Method], Uri=[$uri], Response=[$responseBody]. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertFrom-TestJson {
|
||||
param([string]$Json)
|
||||
|
||||
$Json | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-TestDefinition {
|
||||
param(
|
||||
[object[]]$Definitions,
|
||||
[string]$RevisionId,
|
||||
[string]$Kind,
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
@($Definitions | Where-Object {
|
||||
[string]$_.templateRevisionId -eq $RevisionId -and
|
||||
$_.kind -eq $Kind -and
|
||||
$_.name -eq $Name
|
||||
})[0]
|
||||
}
|
||||
|
||||
function Get-TestFirst {
|
||||
param([object[]]$Items)
|
||||
|
||||
@($Items | Select-Object -First 1)[0]
|
||||
}
|
||||
|
||||
function Get-TestSelectionByRole {
|
||||
param(
|
||||
[object]$Composition,
|
||||
[string]$Role
|
||||
)
|
||||
|
||||
Get-TestFirst -Items @($Composition.templateSelections | Where-Object { $_.templateRole -eq $Role })
|
||||
}
|
||||
|
||||
function Get-TestRevisionDefinitions {
|
||||
param([object]$Selection)
|
||||
|
||||
$templateId = [string]$Selection.templateVersion.templateId
|
||||
$versionId = [string]$Selection.templateVersionId
|
||||
$revisionId = [string]$Selection.templateRevisionId
|
||||
|
||||
@(Invoke-TestMergeApiJson -Method GET -Path "Template/$templateId/Versions/$versionId/Revisions/$revisionId/Definitions")
|
||||
}
|
||||
|
||||
function Assert-TestApiIsReachable {
|
||||
try {
|
||||
Invoke-TestMergeApiJson -Method GET -Path "deployment-batches/$DeploymentBatchId/composition" | Out-Null
|
||||
}
|
||||
catch {
|
||||
if ($_.Exception.Message -match '401|Nicht autorisiert|Unauthorized') {
|
||||
throw @"
|
||||
API authentication failed for [$ApiBaseUrl].
|
||||
Start the API with Windows authentication enabled and run the test from a session that can authenticate to it.
|
||||
You can also pass explicit credentials:
|
||||
Invoke-Pester -Script @{ Path = '.Net\Microsoft.SelfService.Portal.Core.API\.tests\Api.Tests.ps1'; Parameters = @{ Credential = (Get-Credential) } }
|
||||
|
||||
$($_.Exception.Message)
|
||||
"@
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
Describe ' API DemoData composition' {
|
||||
BeforeAll {
|
||||
Assert-TestApiIsReachable
|
||||
|
||||
$script:expectedTemplateAliases = @(
|
||||
'Environment-Test'
|
||||
'Domain-Contoso'
|
||||
'Service-SharePoint-Contoso'
|
||||
'Stage-Install'
|
||||
)
|
||||
|
||||
$script:expectedTemplateRoles = @(
|
||||
'Environment'
|
||||
'Domain'
|
||||
'Service'
|
||||
'Stage'
|
||||
)
|
||||
|
||||
$script:expectedTemplateRevisionIds = @(
|
||||
'72000000-0000-0000-0000-000000000101'
|
||||
'72000000-0000-0000-0000-000000000102'
|
||||
'72000000-0000-0000-0000-000000000103'
|
||||
'72000000-0000-0000-0000-000000000104'
|
||||
)
|
||||
|
||||
$script:apiComposition = Invoke-TestMergeApiJson -Method GET -Path "deployment-batches/$DeploymentBatchId/composition"
|
||||
$script:environmentSelection = Get-TestSelectionByRole -Composition $script:apiComposition -Role 'Environment'
|
||||
$script:domainSelection = Get-TestSelectionByRole -Composition $script:apiComposition -Role 'Domain'
|
||||
$script:sharePointSelection = Get-TestSelectionByRole -Composition $script:apiComposition -Role 'Service'
|
||||
$script:stageSelection = Get-TestSelectionByRole -Composition $script:apiComposition -Role 'Stage'
|
||||
$script:environmentRevisionId = [string]$script:environmentSelection.templateRevisionId
|
||||
$script:domainRevisionId = [string]$script:domainSelection.templateRevisionId
|
||||
$script:sharePointRevisionId = [string]$script:sharePointSelection.templateRevisionId
|
||||
$script:environmentDefinitions = Get-TestRevisionDefinitions -Selection $script:environmentSelection
|
||||
$script:domainDefinitions = Get-TestRevisionDefinitions -Selection $script:domainSelection
|
||||
$script:sharePointDefinitions = Get-TestRevisionDefinitions -Selection $script:sharePointSelection
|
||||
$script:stageDefinitions = Get-TestRevisionDefinitions -Selection $script:stageSelection
|
||||
$script:deploymentArtifacts = @(Invoke-TestMergeApiJson -Method GET -Path "deployment-artifacts?deploymentGroupId=$DeploymentBatchId")
|
||||
$script:credentialSecrets = @(Invoke-TestMergeApiJson -Method GET -Path 'credential-secrets')
|
||||
$script:configurationDefinitions = @(Invoke-TestMergeApiJson -Method GET -Path 'configuration-definitions')
|
||||
$script:parameterDefinitions = @(Invoke-TestMergeApiJson -Method GET -Path 'configuration-definitions?kind=Parameter')
|
||||
$script:variableDefinitions = @(Invoke-TestMergeApiJson -Method GET -Path 'configuration-definitions?kind=Variable')
|
||||
$script:configurationValues = @(Invoke-TestMergeApiJson -Method GET -Path 'configuration-values')
|
||||
}
|
||||
|
||||
It 'loads the seeded deployment composition from the API' {
|
||||
$script:apiComposition.deploymentBatchId | Should Be $DeploymentBatchId
|
||||
@($script:apiComposition.templateSelections).Count | Should Be 4
|
||||
@($script:apiComposition.targetAssignments).Count | Should Be 3
|
||||
}
|
||||
|
||||
It 'keeps the template selection order from the seeded deployment' {
|
||||
$aliases = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.alias })
|
||||
$roles = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.templateRole })
|
||||
|
||||
($aliases -join '|') | Should Be ($script:expectedTemplateAliases -join '|')
|
||||
($roles -join '|') | Should Be ($script:expectedTemplateRoles -join '|')
|
||||
}
|
||||
|
||||
It 'pins every template selection to an immutable template revision' {
|
||||
$revisionIds = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { [string]$_.templateRevisionId })
|
||||
|
||||
($revisionIds -join '|') | Should Be ($script:expectedTemplateRevisionIds -join '|')
|
||||
@($revisionIds | Where-Object { [string]::IsNullOrWhiteSpace($_) }).Count | Should Be 0
|
||||
}
|
||||
|
||||
It 'loads each pinned template revision and its definitions from the API' {
|
||||
foreach ($selection in @($script:apiComposition.templateSelections | Sort-Object sortOrder)) {
|
||||
$templateId = [string]$selection.templateVersion.templateId
|
||||
$versionId = [string]$selection.templateVersionId
|
||||
$revisionId = [string]$selection.templateRevisionId
|
||||
|
||||
$revision = Invoke-TestMergeApiJson -Method GET -Path "Template/$templateId/Versions/$versionId/Revisions/$revisionId"
|
||||
$definitions = @(Invoke-TestMergeApiJson -Method GET -Path "Template/$templateId/Versions/$versionId/Revisions/$revisionId/Definitions")
|
||||
|
||||
$revision.id | Should Be $revisionId
|
||||
$revision.templateVersionId | Should Be $versionId
|
||||
$revision.revisionNumber | Should Be 1
|
||||
$revision.isCurrent | Should Be $true
|
||||
$revision.isPublished | Should Be $true
|
||||
|
||||
if ($selection.templateRole -ne 'Stage') {
|
||||
@($definitions).Count | Should BeGreaterThan 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
It 'exposes deployment artifact and credential store API surfaces' {
|
||||
@($script:deploymentArtifacts).Count | Should Be 1
|
||||
$script:deploymentArtifacts[0].deploymentGroupId | Should Be $DeploymentBatchId
|
||||
$script:deploymentArtifacts[0].artifactType | Should Be 'ResolvedConfigurationData'
|
||||
$script:deploymentArtifacts[0].isStale | Should Be $false
|
||||
|
||||
$secretNames = @($script:credentialSecrets | ForEach-Object { $_.name } | Sort-Object)
|
||||
$secretNames -contains 'Windows/SharePoint/SetupAccount' | Should Be $true
|
||||
$secretNames -contains 'Windows/SharePoint/FarmAccount' | Should Be $true
|
||||
$secretNames -contains 'Windows/SharePoint/FarmPassphrase' | Should Be $true
|
||||
$secretNames -contains 'Windows/SharePoint/DefaultServiceAccount' | Should Be $true
|
||||
$secretNames -contains 'Windows/SharePoint/SearchAccount' | Should Be $true
|
||||
}
|
||||
|
||||
It 'returns resolved deployment artifact values without reading local PSD1 files' {
|
||||
$deploymentJson = ConvertFrom-TestJson -Json $script:deploymentArtifacts[0].deploymentJson
|
||||
|
||||
$deploymentJson.variables.DatabasePrefix | Should Be 'SharePoint_Contoso_Test'
|
||||
$deploymentJson.variables.ServiceDbPrefix | Should Be 'SharePoint_Contoso_Test_Services'
|
||||
$deploymentJson.variables.ConfigDbName | Should Be 'SharePoint_Contoso_Test_Farm_Config'
|
||||
$deploymentJson.resources.NonNodeData.LocalConfigurationManager.ConfigurationMode | Should Be 'ApplyOnly'
|
||||
@($deploymentJson.resources.AllNodes).Count | Should Be 3
|
||||
$deploymentJson.resources.AllNodes[0].NodeName | Should Be 'CLD-SHP-01'
|
||||
$deploymentJson.resources.AllNodes[0].RunCentralAdministration | Should Be $true
|
||||
}
|
||||
|
||||
It 'seeds parameter and variable definitions in the Configuration Definition API' {
|
||||
@($script:configurationDefinitions).Count | Should BeGreaterThan 0
|
||||
@($script:parameterDefinitions).Count | Should BeGreaterThan 0
|
||||
@($script:variableDefinitions).Count | Should BeGreaterThan 0
|
||||
|
||||
$landscape = Get-TestDefinition -Definitions $script:environmentDefinitions -RevisionId $script:environmentRevisionId -Kind 'Parameter' -Name 'Landscape'
|
||||
$domainFqdn = Get-TestDefinition -Definitions $script:domainDefinitions -RevisionId $script:domainRevisionId -Kind 'Parameter' -Name 'DomainFQDN'
|
||||
$databasePrefix = Get-TestDefinition -Definitions $script:sharePointDefinitions -RevisionId $script:sharePointRevisionId -Kind 'Parameter' -Name 'DatabasePrefix'
|
||||
$configDbName = Get-TestDefinition -Definitions $script:environmentDefinitions -RevisionId $script:environmentRevisionId -Kind 'Variable' -Name 'ConfigDbName'
|
||||
|
||||
$landscape.id | Should Not BeNullOrEmpty
|
||||
$domainFqdn.id | Should Not BeNullOrEmpty
|
||||
$databasePrefix.id | Should Not BeNullOrEmpty
|
||||
$configDbName.id | Should Not BeNullOrEmpty
|
||||
|
||||
(ConvertFrom-TestJson -Json $landscape.propertiesJson).AllowedValues -contains 'Test' | Should Be $true
|
||||
(ConvertFrom-TestJson -Json $databasePrefix.propertiesJson).DefaultValue | Should Be 'SharePoint'
|
||||
(ConvertFrom-TestJson -Json $configDbName.propertiesJson).Expression | Should Match 'joinNotEmpty'
|
||||
}
|
||||
|
||||
It 'exposes configuration definitions through the standalone API and keeps revision definitions typed' {
|
||||
$environmentDefinitions = @(Invoke-TestMergeApiJson -Method GET -Path "configuration-definitions?templateRevisionId=$($script:environmentRevisionId)")
|
||||
$environmentVariables = @($script:environmentDefinitions | Where-Object { $_.kind -eq 'Variable' })
|
||||
|
||||
@($environmentDefinitions).Count | Should BeGreaterThan 0
|
||||
@($environmentVariables).Count | Should BeGreaterThan 0
|
||||
@($environmentVariables | Where-Object { $_.kind -ne 'Variable' }).Count | Should Be 0
|
||||
|
||||
$environmentDefinitionNames = @($script:environmentDefinitions | ForEach-Object { $_.name })
|
||||
$environmentVariableNames = @($environmentVariables | ForEach-Object { $_.name })
|
||||
$environmentDefinitionNames -contains 'Landscape' | Should Be $true
|
||||
$environmentVariableNames -contains 'ConfigDbName' | Should Be $true
|
||||
}
|
||||
|
||||
It 'seeds configuration values for parameters and variables' {
|
||||
@($script:configurationValues).Count | Should BeGreaterThan 0
|
||||
|
||||
$landscape = Get-TestDefinition -Definitions $script:environmentDefinitions -RevisionId $script:environmentRevisionId -Kind 'Parameter' -Name 'Landscape'
|
||||
$domainFqdn = Get-TestDefinition -Definitions $script:domainDefinitions -RevisionId $script:domainRevisionId -Kind 'Parameter' -Name 'DomainFQDN'
|
||||
$configDbName = Get-TestDefinition -Definitions $script:environmentDefinitions -RevisionId $script:environmentRevisionId -Kind 'Variable' -Name 'ConfigDbName'
|
||||
|
||||
$landscapeValues = @(Invoke-TestMergeApiJson -Method GET -Path "configuration-definitions/$($landscape.id)/values")
|
||||
$domainFqdnValues = @(Invoke-TestMergeApiJson -Method GET -Path "configuration-values?definitionId=$($domainFqdn.id)")
|
||||
$configDbNameValues = @(Invoke-TestMergeApiJson -Method GET -Path "configuration-values?definitionId=$($configDbName.id)")
|
||||
|
||||
@($landscapeValues).Count | Should Be 1
|
||||
@($domainFqdnValues).Count | Should Be 1
|
||||
@($configDbNameValues).Count | Should Be 1
|
||||
|
||||
(ConvertFrom-TestJson -Json $landscapeValues[0].valueJson).value | Should Be 'Test'
|
||||
(ConvertFrom-TestJson -Json $domainFqdnValues[0].valueJson).value | Should Be 'contoso.local'
|
||||
(ConvertFrom-TestJson -Json $configDbNameValues[0].valueJson).expression | Should Match 'joinNotEmpty'
|
||||
|
||||
$landscapeValues[0].scopeType | Should Be 'TemplateRevision'
|
||||
[string]$landscapeValues[0].scopeId | Should Be $script:environmentRevisionId
|
||||
$configDbNameValues[0].valueSourceType | Should Be 'Expression'
|
||||
}
|
||||
|
||||
It 'marks sensitive parameter values as secret references' {
|
||||
$setupCredential = Get-TestDefinition -Definitions $script:sharePointDefinitions -RevisionId $script:sharePointRevisionId -Kind 'Parameter' -Name 'SetupCredential'
|
||||
$setupCredentialValues = @(Invoke-TestMergeApiJson -Method GET -Path "configuration-values?definitionId=$($setupCredential.id)")
|
||||
$setupValue = ConvertFrom-TestJson -Json $setupCredentialValues[0].valueJson
|
||||
|
||||
@($setupCredentialValues).Count | Should Be 1
|
||||
$setupCredentialValues[0].valueSourceType | Should Be 'SecretReference'
|
||||
$setupValue.value.Provider | Should Be 'SecretManagement'
|
||||
$setupValue.value.Name | Should Be 'Windows/SharePoint/SetupAccount'
|
||||
}
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
param(
|
||||
[string]$ApiBaseUrl = 'http://localhost:5286/api',
|
||||
|
||||
[string]$BgwRoot = ('F:\Kunden Auftr' + [char]0x00E4 + 'ge\BGW'),
|
||||
|
||||
[string]$DeploymentBatchId = '80000000-0000-0000-0000-000000000101',
|
||||
|
||||
[string]$MergeModulePath = 'F:\Projekte\Coding\PowerShell\Merge-DSCConfigurationData\Merge-DSCConfigurationData.psd1',
|
||||
|
||||
[string]$ResolveModulePath = 'F:\Projekte\Coding\PowerShell\Resolve-DSCConfigurationData\Resolve-DSCConfigurationData.psd1',
|
||||
|
||||
[int]$ApiTimeoutSec = 30,
|
||||
|
||||
[bool]$UseDefaultCredentials = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Invoke-TestBgwMergeApiJson {
|
||||
param(
|
||||
[ValidateSet('GET', 'POST', 'PUT', 'DELETE')]
|
||||
[string]$Method,
|
||||
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/'))
|
||||
$parameters = @{
|
||||
Method = $Method
|
||||
Uri = $uri
|
||||
TimeoutSec = $ApiTimeoutSec
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
|
||||
if ($UseDefaultCredentials) {
|
||||
$parameters.UseDefaultCredentials = $true
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-RestMethod @parameters
|
||||
}
|
||||
catch {
|
||||
$responseBody = '<empty response body>'
|
||||
if ($_.Exception.Response) {
|
||||
try {
|
||||
$stream = $_.Exception.Response.GetResponseStream()
|
||||
if ($stream) {
|
||||
$reader = [System.IO.StreamReader]::new($stream)
|
||||
$text = $reader.ReadToEnd()
|
||||
if (-not [string]::IsNullOrWhiteSpace($text)) {
|
||||
$responseBody = $text
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$responseBody = '<could not read response body>'
|
||||
}
|
||||
}
|
||||
|
||||
throw "API request failed. Method=[$Method], Uri=[$uri], Response=[$responseBody]. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertTo-TestBgwMergeConfigurationValue {
|
||||
param([object]$Value)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($Value -is [string] -or $Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
|
||||
return $Value
|
||||
}
|
||||
|
||||
if ($Value -is [System.Collections.IDictionary]) {
|
||||
$result = [ordered]@{}
|
||||
foreach ($key in $Value.Keys) {
|
||||
$result[[string]$key] = ConvertTo-TestBgwMergeConfigurationValue -Value $Value[$key]
|
||||
}
|
||||
|
||||
Write-Output -NoEnumerate $result
|
||||
return
|
||||
}
|
||||
|
||||
if ($Value -is [pscustomobject]) {
|
||||
$result = [ordered]@{}
|
||||
foreach ($property in $Value.PSObject.Properties) {
|
||||
$result[$property.Name] = ConvertTo-TestBgwMergeConfigurationValue -Value $property.Value
|
||||
}
|
||||
|
||||
Write-Output -NoEnumerate $result
|
||||
return
|
||||
}
|
||||
|
||||
if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
|
||||
$items = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($item in $Value) {
|
||||
$items.Add((ConvertTo-TestBgwMergeConfigurationValue -Value $item))
|
||||
}
|
||||
|
||||
Write-Output -NoEnumerate $items.ToArray()
|
||||
return
|
||||
}
|
||||
|
||||
return $Value
|
||||
}
|
||||
|
||||
function ConvertFrom-TestBgwMergeTemplateJson {
|
||||
param([string]$JsonData)
|
||||
|
||||
$document = $JsonData | ConvertFrom-Json
|
||||
$metadata = ConvertTo-TestBgwMergeConfigurationValue -Value $document.metadata
|
||||
if ($null -eq $metadata) {
|
||||
$metadata = [ordered]@{}
|
||||
}
|
||||
|
||||
if (-not $metadata.Contains('TemplateType')) {
|
||||
$metadata['TemplateType'] = $document.templateType
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
Metadata = $metadata
|
||||
Parameters = ConvertTo-TestBgwMergeConfigurationValue -Value $document.parameters
|
||||
Variables = ConvertTo-TestBgwMergeConfigurationValue -Value $document.variables
|
||||
Resources = ConvertTo-TestBgwMergeConfigurationValue -Value $document.resources
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertFrom-TestBgwMergeTargetAssignments {
|
||||
param([object[]]$TargetAssignments)
|
||||
|
||||
@($TargetAssignments | Sort-Object sortOrder | ForEach-Object {
|
||||
$nodeData = ConvertTo-TestBgwMergeConfigurationValue -Value ($_.nodeDataJson | ConvertFrom-Json)
|
||||
$node = @{}
|
||||
|
||||
foreach ($key in $nodeData.Keys) {
|
||||
if ($key -eq 'nodeName') {
|
||||
$node.NodeName = $nodeData[$key]
|
||||
}
|
||||
else {
|
||||
$node[$key] = $nodeData[$key]
|
||||
}
|
||||
}
|
||||
|
||||
$node
|
||||
})
|
||||
}
|
||||
|
||||
function ConvertTo-TestBgwMergeStableValue {
|
||||
param([object]$Value)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($Value -is [string] -or $Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
|
||||
return $Value
|
||||
}
|
||||
|
||||
if ($Value -is [System.Collections.IDictionary]) {
|
||||
$result = [ordered]@{}
|
||||
foreach ($key in @($Value.Keys | Sort-Object)) {
|
||||
$result[[string]$key] = ConvertTo-TestBgwMergeStableValue -Value $Value[$key]
|
||||
}
|
||||
|
||||
Write-Output -NoEnumerate $result
|
||||
return
|
||||
}
|
||||
|
||||
if ($Value -is [pscustomobject]) {
|
||||
$result = [ordered]@{}
|
||||
foreach ($property in @($Value.PSObject.Properties | Sort-Object Name)) {
|
||||
$result[$property.Name] = ConvertTo-TestBgwMergeStableValue -Value $property.Value
|
||||
}
|
||||
|
||||
Write-Output -NoEnumerate $result
|
||||
return
|
||||
}
|
||||
|
||||
if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
|
||||
$items = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($item in $Value) {
|
||||
$items.Add((ConvertTo-TestBgwMergeStableValue -Value $item))
|
||||
}
|
||||
|
||||
Write-Output -NoEnumerate $items.ToArray()
|
||||
return
|
||||
}
|
||||
|
||||
return $Value
|
||||
}
|
||||
|
||||
function ConvertTo-TestBgwMergeStableJson {
|
||||
param([object]$Value)
|
||||
|
||||
(ConvertTo-TestBgwMergeStableValue -Value $Value) | ConvertTo-Json -Depth 100 -Compress
|
||||
}
|
||||
|
||||
function Invoke-TestBgwMergeSnapshotScript {
|
||||
param([string]$Script)
|
||||
|
||||
$encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Script))
|
||||
$output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encodedCommand
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Snapshot PowerShell process failed with exit code [$LASTEXITCODE]. Output: $($output -join [Environment]::NewLine)"
|
||||
}
|
||||
|
||||
$json = ($output | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join [Environment]::NewLine
|
||||
if ([string]::IsNullOrWhiteSpace($json)) {
|
||||
throw 'Snapshot PowerShell process returned no JSON output.'
|
||||
}
|
||||
|
||||
$json | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function New-TestBgwMergeSnapshotScript {
|
||||
param(
|
||||
[string]$ConfigurationDataPath,
|
||||
[switch]$BuildLocalBaseline
|
||||
)
|
||||
|
||||
$escapedBgwRoot = $BgwRoot.Replace("'", "''")
|
||||
$escapedMergeModulePath = $MergeModulePath.Replace("'", "''")
|
||||
$escapedResolveModulePath = $ResolveModulePath.Replace("'", "''")
|
||||
$escapedConfigurationDataPath = if ($ConfigurationDataPath) { $ConfigurationDataPath.Replace("'", "''") } else { '' }
|
||||
$buildLocalBaselineText = if ($BuildLocalBaseline) { 'True' } else { 'False' }
|
||||
|
||||
@"
|
||||
`$ErrorActionPreference = 'Stop'
|
||||
`$ProgressPreference = 'SilentlyContinue'
|
||||
Import-Module '$escapedMergeModulePath' -Force
|
||||
Import-Module '$escapedResolveModulePath' -Force
|
||||
|
||||
function ConvertTo-SnapshotStableValue {
|
||||
param([object]`$Value)
|
||||
|
||||
if (`$null -eq `$Value) { return `$null }
|
||||
if (`$Value -is [string] -or `$Value -is [bool] -or `$Value -is [int] -or `$Value -is [long] -or `$Value -is [double] -or `$Value -is [decimal]) { return `$Value }
|
||||
|
||||
if (`$Value -is [System.Collections.IDictionary]) {
|
||||
`$result = [ordered]@{}
|
||||
foreach (`$key in @(`$Value.Keys | Sort-Object)) {
|
||||
`$result[[string]`$key] = ConvertTo-SnapshotStableValue -Value `$Value[`$key]
|
||||
}
|
||||
Write-Output -NoEnumerate `$result
|
||||
return
|
||||
}
|
||||
|
||||
if (`$Value -is [pscustomobject]) {
|
||||
`$result = [ordered]@{}
|
||||
foreach (`$property in @(`$Value.PSObject.Properties | Sort-Object Name)) {
|
||||
`$result[`$property.Name] = ConvertTo-SnapshotStableValue -Value `$property.Value
|
||||
}
|
||||
Write-Output -NoEnumerate `$result
|
||||
return
|
||||
}
|
||||
|
||||
if (`$Value -is [System.Collections.IEnumerable] -and `$Value -isnot [string]) {
|
||||
`$items = New-Object System.Collections.Generic.List[object]
|
||||
foreach (`$item in `$Value) {
|
||||
`$items.Add((ConvertTo-SnapshotStableValue -Value `$item))
|
||||
}
|
||||
Write-Output -NoEnumerate `$items.ToArray()
|
||||
return
|
||||
}
|
||||
|
||||
return `$Value
|
||||
}
|
||||
|
||||
function ConvertTo-SnapshotStableJson {
|
||||
param([object]`$Value)
|
||||
(ConvertTo-SnapshotStableValue -Value `$Value) | ConvertTo-Json -Depth 100 -Compress
|
||||
}
|
||||
|
||||
if ('$buildLocalBaselineText' -eq 'True') {
|
||||
`$root = '$escapedBgwRoot'
|
||||
`$sourceTemplates = @(
|
||||
(Join-Path -Path `$root -ChildPath 'Environment\Test.psd1')
|
||||
(Join-Path -Path `$root -ChildPath 'Domain\Contoso.psd1')
|
||||
(Join-Path -Path `$root -ChildPath 'Service\SharePoint\Contoso.psd1')
|
||||
(Join-Path -Path `$root -ChildPath 'Stage\Install.psd1')
|
||||
)
|
||||
|
||||
`$allNodes = @(
|
||||
@{ NodeName = 'CLD-SHP-01'; RunCentralAdministration = `$true }
|
||||
@{ NodeName = 'CLD-SHP-02'; RunCentralAdministration = `$false }
|
||||
@{ NodeName = 'CLD-SHP-03'; RunCentralAdministration = `$false }
|
||||
)
|
||||
|
||||
`$configurationData = New-DSCConfigurationDataDeployment -Name 'Contoso-Test-SharePoint' -DeploymentId '8f6c2c1a' -SourceTemplatePath `$sourceTemplates -AllNodes `$allNodes
|
||||
}
|
||||
else {
|
||||
`$configurationData = Import-PowerShellDataFile -LiteralPath '$escapedConfigurationDataPath'
|
||||
}
|
||||
|
||||
`$resolved = Resolve-DSCConfigurationData -ConfigurationData `$configurationData -SkipSecrets
|
||||
`$snapshot = [ordered]@{
|
||||
ParametersJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Parameters
|
||||
VariablesJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Variables
|
||||
ResourcesJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Resources
|
||||
DatabasePrefix = `$resolved.Variables.DatabasePrefix
|
||||
ServiceDbPrefix = `$resolved.Variables.ServiceDbPrefix
|
||||
ConfigDbName = `$resolved.Variables.ConfigDbName
|
||||
LcmConfigurationMode = `$resolved.Resources.NonNodeData.LocalConfigurationManager.ConfigurationMode
|
||||
AllNodes = @(`$resolved.Resources.AllNodes | Sort-Object NodeName | ForEach-Object {
|
||||
[ordered]@{
|
||||
NodeName = `$_.NodeName
|
||||
RunCentralAdministration = `$_.RunCentralAdministration
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
`$snapshot | ConvertTo-Json -Depth 100 -Compress
|
||||
"@
|
||||
}
|
||||
|
||||
function New-TestBgwMergeLocalSnapshot {
|
||||
Invoke-TestBgwMergeSnapshotScript -Script (New-TestBgwMergeSnapshotScript -BuildLocalBaseline)
|
||||
}
|
||||
|
||||
function New-TestBgwMergeSnapshotFromConfigurationData {
|
||||
param([hashtable]$ConfigurationData)
|
||||
|
||||
Import-Module $MergeModulePath -Force
|
||||
|
||||
$tempPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ("BgwMergeApiSnapshot-{0}.psd1" -f ([guid]::NewGuid().ToString('N')))
|
||||
try {
|
||||
Export-PowerShellDataFile -InputObject $ConfigurationData -Path $tempPath -Force
|
||||
Invoke-TestBgwMergeSnapshotScript -Script (New-TestBgwMergeSnapshotScript -ConfigurationDataPath $tempPath)
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $tempPath) {
|
||||
Remove-Item -LiteralPath $tempPath -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function New-TestBgwMergeApiConfigurationData {
|
||||
param([object]$Composition)
|
||||
|
||||
Import-Module $MergeModulePath -Force
|
||||
|
||||
$mergedTemplateData = @{}
|
||||
|
||||
foreach ($selection in @($Composition.templateSelections | Sort-Object sortOrder)) {
|
||||
$templateData = ConvertFrom-TestBgwMergeTemplateJson -JsonData $selection.templateVersion.jsonData
|
||||
$mergedTemplateData = Merge-DSCConfigurationData -Template $mergedTemplateData -Deployment $templateData
|
||||
}
|
||||
|
||||
$deploymentData = @{
|
||||
Resources = @{
|
||||
AllNodes = ConvertFrom-TestBgwMergeTargetAssignments -TargetAssignments @($Composition.targetAssignments)
|
||||
}
|
||||
}
|
||||
|
||||
Merge-DSCConfigurationData -Template $mergedTemplateData -Deployment $deploymentData
|
||||
}
|
||||
|
||||
Describe 'BGW Test.Merge.ps1 API DemoData composition' {
|
||||
BeforeAll {
|
||||
$script:expectedTemplateAliases = @(
|
||||
'Environment-Test'
|
||||
'Domain-Contoso'
|
||||
'Service-SharePoint-Contoso'
|
||||
'Stage-Install'
|
||||
)
|
||||
|
||||
$script:expectedTemplateRoles = @(
|
||||
'Environment'
|
||||
'Domain'
|
||||
'Service'
|
||||
'Stage'
|
||||
)
|
||||
|
||||
$script:localSnapshot = New-TestBgwMergeLocalSnapshot
|
||||
$script:apiComposition = Invoke-TestBgwMergeApiJson -Method GET -Path "deployment-batches/$DeploymentBatchId/composition"
|
||||
$script:apiConfigurationData = New-TestBgwMergeApiConfigurationData -Composition $script:apiComposition
|
||||
$script:apiSnapshot = New-TestBgwMergeSnapshotFromConfigurationData -ConfigurationData $script:apiConfigurationData
|
||||
}
|
||||
|
||||
It 'loads the seeded deployment composition from the API' {
|
||||
$script:apiComposition.deploymentBatchId | Should Be $DeploymentBatchId
|
||||
@($script:apiComposition.templateSelections).Count | Should Be 4
|
||||
@($script:apiComposition.targetAssignments).Count | Should Be 3
|
||||
}
|
||||
|
||||
It 'keeps the template selection order from Test.Merge.ps1' {
|
||||
$aliases = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.alias })
|
||||
$roles = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.templateRole })
|
||||
|
||||
($aliases -join '|') | Should Be ($script:expectedTemplateAliases -join '|')
|
||||
($roles -join '|') | Should Be ($script:expectedTemplateRoles -join '|')
|
||||
}
|
||||
|
||||
It 'keeps the AllNodes data from Test.Merge.ps1' {
|
||||
$nodes = @($script:apiSnapshot.AllNodes | Sort-Object NodeName)
|
||||
|
||||
$nodes.Count | Should Be 3
|
||||
$nodes[0].NodeName | Should Be 'CLD-SHP-01'
|
||||
$nodes[0].RunCentralAdministration | Should Be $true
|
||||
$nodes[1].NodeName | Should Be 'CLD-SHP-02'
|
||||
$nodes[1].RunCentralAdministration | Should Be $false
|
||||
$nodes[2].NodeName | Should Be 'CLD-SHP-03'
|
||||
$nodes[2].RunCentralAdministration | Should Be $false
|
||||
}
|
||||
|
||||
It 'resolves the same core values as the local Test.Merge.ps1 baseline' {
|
||||
$script:apiSnapshot.DatabasePrefix | Should Be $script:localSnapshot.DatabasePrefix
|
||||
$script:apiSnapshot.ServiceDbPrefix | Should Be $script:localSnapshot.ServiceDbPrefix
|
||||
$script:apiSnapshot.ConfigDbName | Should Be $script:localSnapshot.ConfigDbName
|
||||
$script:apiSnapshot.LcmConfigurationMode | Should Be $script:localSnapshot.LcmConfigurationMode
|
||||
}
|
||||
|
||||
It 'matches merged Parameters, Variables and Resources' {
|
||||
$script:apiSnapshot.ParametersJson | Should Be $script:localSnapshot.ParametersJson
|
||||
$script:apiSnapshot.VariablesJson | Should Be $script:localSnapshot.VariablesJson
|
||||
$script:apiSnapshot.ResourcesJson | Should Be $script:localSnapshot.ResourcesJson
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ApiBaseUrl = 'http://localhost:5286/api',
|
||||
|
||||
[string]$BgwRoot = ('F:\Kunden Auftr' + [char]0x00E4 + 'ge\BGW'),
|
||||
|
||||
[string]$DeploymentBatchId = '80000000-0000-0000-0000-000000000101',
|
||||
|
||||
[string]$MergeModulePath = 'F:\Projekte\Coding\PowerShell\Merge-DSCConfigurationData\Merge-DSCConfigurationData.psd1',
|
||||
|
||||
[string]$ResolveModulePath = 'F:\Projekte\Coding\PowerShell\Resolve-DSCConfigurationData\Resolve-DSCConfigurationData.psd1',
|
||||
|
||||
[int]$ApiTimeoutSec = 30,
|
||||
|
||||
[switch]$UseDefaultCredentials = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$testPath = Join-Path -Path $PSScriptRoot -ChildPath 'BgwMerge.Api.Tests.ps1'
|
||||
if (-not (Test-Path -LiteralPath $testPath -PathType Leaf)) {
|
||||
throw "Pester test file [$testPath] was not found."
|
||||
}
|
||||
|
||||
if (-not (Get-Command Invoke-Pester -ErrorAction SilentlyContinue)) {
|
||||
throw 'Pester is required. Install-Module Pester or run this on a machine where Pester is available.'
|
||||
}
|
||||
|
||||
$parameters = @{
|
||||
ApiBaseUrl = $ApiBaseUrl
|
||||
BgwRoot = $BgwRoot
|
||||
DeploymentBatchId = $DeploymentBatchId
|
||||
MergeModulePath = $MergeModulePath
|
||||
ResolveModulePath = $ResolveModulePath
|
||||
ApiTimeoutSec = $ApiTimeoutSec
|
||||
UseDefaultCredentials = [bool]$UseDefaultCredentials
|
||||
}
|
||||
|
||||
$result = Invoke-Pester -Script @{ Path = $testPath; Parameters = $parameters } -PassThru
|
||||
if ($result.FailedCount -gt 0) {
|
||||
throw "Pester test run failed. Passed=[$($result.PassedCount)] Failed=[$($result.FailedCount)] Skipped=[$($result.SkippedCount)]."
|
||||
}
|
||||
137
.tests/OnPremClientCredentials.Api.Tests.ps1
Normal file
137
.tests/OnPremClientCredentials.Api.Tests.ps1
Normal file
@@ -0,0 +1,137 @@
|
||||
param(
|
||||
[string]$ApiBaseUrl = 'http://localhost:5286/api',
|
||||
|
||||
[string]$ClientId = 'ssp-demo-worker',
|
||||
|
||||
[string]$ClientSecret = 'DemoOnly-DoNotUseInProduction!',
|
||||
|
||||
[int]$ApiTimeoutSec = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Invoke-TestApi {
|
||||
param(
|
||||
[ValidateSet('GET', 'POST', 'DELETE')]
|
||||
[string]$Method,
|
||||
|
||||
[string]$Path,
|
||||
|
||||
[object]$Body,
|
||||
|
||||
[string]$BearerToken
|
||||
)
|
||||
|
||||
$uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/'))
|
||||
$parameters = @{
|
||||
Method = $Method
|
||||
Uri = $uri
|
||||
TimeoutSec = $ApiTimeoutSec
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
|
||||
if ($Body) {
|
||||
$parameters.Body = ($Body | ConvertTo-Json -Depth 10)
|
||||
$parameters.ContentType = 'application/json'
|
||||
}
|
||||
|
||||
if ($BearerToken) {
|
||||
$parameters.Headers = @{
|
||||
Authorization = "Bearer $BearerToken"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-RestMethod @parameters
|
||||
}
|
||||
catch {
|
||||
$responseBody = '<empty response body>'
|
||||
if ($_.Exception.Response) {
|
||||
try {
|
||||
$stream = $_.Exception.Response.GetResponseStream()
|
||||
if ($stream) {
|
||||
$reader = [System.IO.StreamReader]::new($stream)
|
||||
$text = $reader.ReadToEnd()
|
||||
if (-not [string]::IsNullOrWhiteSpace($text)) {
|
||||
$responseBody = $text
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$responseBody = '<could not read response body>'
|
||||
}
|
||||
}
|
||||
|
||||
throw "API request failed. Method=[$Method], Uri=[$uri], Response=[$responseBody]. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'On-prem client credentials API authentication' {
|
||||
It 'issues a bearer token for the seeded demo worker client' {
|
||||
$token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
|
||||
clientId = $ClientId
|
||||
clientSecret = $ClientSecret
|
||||
scope = 'deployment.read template.read'
|
||||
}
|
||||
|
||||
$token.accessToken | Should Not BeNullOrEmpty
|
||||
$token.tokenType | Should Be 'Bearer'
|
||||
$token.expiresIn | Should BeGreaterThan 0
|
||||
$token.scope | Should Be 'deployment.read template.read'
|
||||
}
|
||||
|
||||
It 'allows bearer-token API calls without Windows authentication' {
|
||||
$token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
|
||||
clientId = $ClientId
|
||||
clientSecret = $ClientSecret
|
||||
scope = 'deployment.read template.read'
|
||||
}
|
||||
|
||||
$definitions = @(Invoke-TestApi -Method GET -Path 'configuration-definitions?kind=Parameter' -BearerToken $token.accessToken)
|
||||
|
||||
@($definitions).Count | Should BeGreaterThan 0
|
||||
@($definitions | Where-Object { $_.kind -ne 'Parameter' }).Count | Should Be 0
|
||||
}
|
||||
|
||||
It 'creates, lists and revokes managed tokens through the token API' {
|
||||
$adminToken = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{
|
||||
clientId = $ClientId
|
||||
clientSecret = $ClientSecret
|
||||
scope = 'token.manage token.admin deployment.read'
|
||||
}
|
||||
|
||||
$managedToken = Invoke-TestApi -Method POST -Path 'tokens/my' -BearerToken $adminToken.accessToken -Body @{
|
||||
name = 'Pester managed token'
|
||||
scope = 'deployment.read'
|
||||
}
|
||||
|
||||
$managedToken.id | Should Not BeNullOrEmpty
|
||||
$managedToken.accessToken | Should Not BeNullOrEmpty
|
||||
$managedToken.scope | Should Be 'deployment.read'
|
||||
|
||||
$myTokens = @(Invoke-TestApi -Method GET -Path 'tokens/my' -BearerToken $adminToken.accessToken)
|
||||
@($myTokens | Where-Object { $_.id -eq $managedToken.id }).Count | Should Be 1
|
||||
|
||||
$allTokens = @(Invoke-TestApi -Method GET -Path 'tokens/admin' -BearerToken $adminToken.accessToken)
|
||||
@($allTokens | Where-Object { $_.id -eq $managedToken.id }).Count | Should Be 1
|
||||
|
||||
$deletedToken = Invoke-TestApi -Method DELETE -Path ('tokens/my/{0}' -f $managedToken.id) -BearerToken $adminToken.accessToken
|
||||
|
||||
$deletedToken.id | Should Be $managedToken.id
|
||||
$deletedToken.revokedAt | Should Not BeNullOrEmpty
|
||||
$deletedToken.isActive | Should Be $false
|
||||
|
||||
$myTokensAfterDelete = @(Invoke-TestApi -Method GET -Path 'tokens/my' -BearerToken $adminToken.accessToken)
|
||||
$myTokenAfterDelete = $myTokensAfterDelete | Where-Object { [string]$_.id -eq [string]$managedToken.id } | Select-Object -First 1
|
||||
if ($myTokenAfterDelete) {
|
||||
$myTokenAfterDelete.isActive | Should Be $false
|
||||
}
|
||||
|
||||
$adminTokensAfterDelete = @(Invoke-TestApi -Method GET -Path 'tokens/admin' -BearerToken $adminToken.accessToken)
|
||||
$revokedToken = $adminTokensAfterDelete | Where-Object { [string]$_.id -eq [string]$managedToken.id } | Select-Object -First 1
|
||||
if ($revokedToken) {
|
||||
$revokedToken.revokedAt | Should Not BeNullOrEmpty
|
||||
$revokedToken.isActive | Should Be $false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user