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'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user