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:
Torsten Brendgen
2026-07-09 14:44:38 +02:00
parent 1aa2adac08
commit dc93ea0813
72 changed files with 32906 additions and 516 deletions

310
.tests/Api.Tests.ps1 Normal file
View 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'
}
}

View File

@@ -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
}
}

View File

@@ -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)]."
}

View 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
}
}
}