diff --git a/.github/workflows/azure-ipam-testing.yml b/.github/workflows/azure-ipam-testing.yml new file mode 100644 index 0000000..141e53a --- /dev/null +++ b/.github/workflows/azure-ipam-testing.yml @@ -0,0 +1,220 @@ +name: Azure IPAM Testing + +run-name: Azure IPAM Deployment & Testing + +on: + pull_request: + branches: [ main ] + +env: + ACR_NAME: ${{ vars.IPAM_TEST_ACR }} + IPAM_UI_ID: ipam-ui-${{ github.run_id }}-${{ github.run_attempt }} + IPAM_ENGINE_ID: ipam-engine-${{ github.run_id }}-${{ github.run_attempt }} + +jobs: + deploy: + name: Deploy Azure IPAM + runs-on: ubuntu-latest + outputs: + ipamURL: ${{ steps.deployScript.outputs.ipamURL }} + ipamUIAppId: ${{ steps.deployScript.outputs.ipamUIAppId }} + ipamEngineAppId: ${{ steps.deployScript.outputs.ipamEngineAppId }} + ipamSuffix: ${{ steps.deployScript.outputs.ipamSuffix }} + ipamResourceGroup: ${{ steps.deployScript.outputs.ipamResourceGroup }} + steps: + - run: echo "Job triggered by a ${{ github.event_name }} event from ${{ github.head_ref }} to main." + + - name: Install Deployment Prerequisites + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module Az, Microsoft.Graph, powershell-yaml -AllowClobber -Force + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + enable-AzPSSession: true + + - name: Checkout Azure IPAM Code + uses: actions/checkout@v3 + with: + sparse-checkout: | + deploy + engine + ui + lb + + - name: Build Azure IPAM Containers + run: | + az acr build -r $ACR_NAME -t ipam-engine:${{ github.run_id }}-${{ github.run_attempt }} -f ./engine/Dockerfile.deb ./engine + az acr build -r $ACR_NAME -t ipam-func:${{ github.run_id }}-${{ github.run_attempt }} -f ./engine/Dockerfile.func ./engine + az acr build -r $ACR_NAME -t ipam-ui:${{ github.run_id }}-${{ github.run_attempt }} -f ./ui/Dockerfile.deb ./ui + az acr build -r $ACR_NAME -t ipam-lb:${{ github.run_id }}-${{ github.run_attempt }} -f ./lb/Dockerfile ./lb + + - name: Update Docker-Compose YAML + shell: pwsh + run: | + $uiContainer = "$env:ACR_NAME.azurecr.io/ipam-ui:${{ github.run_id }}-${{ github.run_attempt }}" + $engineContainer = "$env:ACR_NAME.azurecr.io/ipam-engine:${{ github.run_id }}-${{ github.run_attempt }}" + $lbContainer = "$env:ACR_NAME.azurecr.io/ipam-lb:${{ github.run_id }}-${{ github.run_attempt }}" + + $composeFile = Get-Content -Path ./docker-compose.prod.yml + $composeYaml = $composeFile | ConvertFrom-Yaml + + $composeYaml['services']['ipam-ui'].image = $uiContainer + $composeYaml['services']['ipam-engine'].image = $engineContainer + $composeYaml['services']['nginx-proxy'].image = $lbContainer + + $composeYaml | ConvertTo-Yaml | Out-File -FilePath ./docker-compose.prod.yml + + - name: Deploy Azure IPAM + working-directory: deploy + id: deployScript + shell: pwsh + run: ./deploy.ps1 -Location "westus3" -UIAppName $env:IPAM_UI_ID -EngineAppName $env:IPAM_ENGINE_ID + + - name: "Upload Logs" + working-directory: logs + id: uploadLogs + env: + AZURE_IPAM_SUFFIX: ${{ steps.deployScript.outputs.ipamSuffix }} + STORAGE_ACCT_RG: ${{ vars.LOGGING_STORAGE_RG }} + STORAGE_ACCT_NAME: ${{ vars.LOGGING_STORAGE_NAME }} + STORAGE_ACCT_CTR: ${{ vars.LOGGING_STORAGE_CTR }} + shell: pwsh + run: | + # $dateStamp = (Get-Date -UFormat "%Y-%m-%d_%I-%M-%S_%p").tostring() + # $archiveName = "ipam-${env:AZURE_IPAM_SUFFIX}-${dateStamp}.zip" + # $archiveName = "ipam-${{ github.run_id }}(${{ github.run_attempt }})-${dateStamp}.zip" + $archiveName = "IPAM-RUN(${{ github.run_id }})-ATTEMPT(${{ github.run_attempt }})-SUFFIX($env:AZURE_IPAM_SUFFIX).zip" + + Compress-Archive -Path ./* -DestinationPath /tmp/$archiveName + + $storage = Get-AzStorageAccount -Name $env:STORAGE_ACCT_NAME -ResourceGroupName $env:STORAGE_ACCT_RG + $context = $storage.Context + + $uploadDetails = @{ + File = "/tmp/$archiveName" + Container = $env:STORAGE_ACCT_CTR + Blob = $archiveName + Context = $context + StandardBlobTier = "Hot" + } + + Set-AzStorageBlobContent @uploadDetails + + Write-Output "logFile=$archiveName" >> $Env:GITHUB_OUTPUT + + - name: "Output Azure IPAM Deployment Details" + env: + DEPLOYMENT_DETAILS: ${{ steps.uploadLogs.outputs.deployDetails }} + shell: pwsh + run: | + $deployDetails = @{ + GITHUB_RUN_ID = "${{ github.run_id }}(${{ github.run_attempt }})" + IPAM_SUFFIX = "${{ steps.deployScript.outputs.ipamSuffix }}" + IPAM_UI_APP_ID = "${{ steps.deployScript.outputs.ipamUIAppId }}" + IPAM_ENGINE_APP_ID = "${{ steps.deployScript.outputs.ipamEngineAppId }}" + IPAM_URL = "${{ steps.deployScript.outputs.ipamURL }}" + IPAM_LOG_FILE = "${{ steps.uploadLogs.outputs.logFile }}" + } + + Write-Host "-------------------" + Write-Host "Deployment Details:" + Write-Host "-------------------" + Write-Host ($deployDetails | Format-Table | Out-String) -NoNewline + Write-Host "-------------------" + + test: + name: Test Azure IPAM + runs-on: ubuntu-latest + needs: [ deploy ] + steps: + - name: Install Testing Prerequisites + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module Az, Pester -AllowClobber -Force + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + enable-AzPSSession: true + + - name: Checkout Azure IPAM Code + uses: actions/checkout@v3 + with: + sparse-checkout: | + tests + + - name: Test Azure IPAM w/ Pester + working-directory: tests + env: + IPAM_RESOURCE_GROUP: ${{ needs.deploy.outputs.ipamResourceGroup }} + IPAM_URL: ${{ needs.deploy.outputs.ipamURL }} + IPAM_ENGINE_APP_ID: ${{ needs.deploy.outputs.ipamEngineAppId }} + shell: pwsh + run: | + Import-Module Pester -PassThru + + $pesterSettings = @{ + Run = @{ + Path = "./azureipam.tests.ps1" + PassThru = $true + SkipRemainingOnFailure = "Run" + } + Output = @{ + Verbosity = "Detailed" + } + } + + $pesterConfig = New-PesterConfiguration -Hashtable $pesterSettings + + Invoke-Pester -Configuration $pesterConfig + + cleanup: + name: Cleanup Azure IPAM + runs-on: ubuntu-latest + needs: [ deploy, test ] + steps: + - name: Install Deployment Prerequisites + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module Az -AllowClobber -Force + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + enable-AzPSSession: true + + - name : Remove Azure IPAM Resources + shell: pwsh + run: | + Remove-AzResourceGroup -Name ${{ needs.deploy.outputs.ipamResourceGroup }} -Force + + - name : Remove Azure IPAM Identities + shell: pwsh + run: | + $tenantId = (Get-AzContext).Tenant.Id + $scope = "/providers/Microsoft.Management/managementGroups/$tenantId" + + $uiApp = Get-AzADApplication -ApplicationId ${{ needs.deploy.outputs.ipamUIAppId }} + $engineApp = Get-AzADApplication -ApplicationId ${{ needs.deploy.outputs.ipamEngineAppId }} + $engineSpn = Get-AzADServicePrincipal -ApplicationId ${{ needs.deploy.outputs.ipamEngineAppId }} + + Remove-AzRoleAssignment -ObjectId $engineSpn.Id -Scope $scope -RoleDefinitionName Reader + + $engineSpn | Remove-AzADServicePrincipal + $uiApp | Remove-AzADApplication + $engineApp | Remove-AzADApplication + + - name: "Remove Azure IPAM Containers" + run: | + az acr repository delete --name $ACR_NAME --repository ipam-engine --yes + az acr repository delete --name $ACR_NAME --repository ipam-func --yes + az acr repository delete --name $ACR_NAME --repository ipam-ui --yes + az acr repository delete --name $ACR_NAME --repository ipam-lb --yes diff --git a/.gitignore b/.gitignore index 423e19e..a2ace75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .env .VSCodeCounter -.github/workflows NOTES.md TODO.md /logs diff --git a/deploy/deploy.ps1 b/deploy/deploy.ps1 index 5c3d1dd..c7189f9 100644 --- a/deploy/deploy.ps1 +++ b/deploy/deploy.ps1 @@ -859,6 +859,8 @@ process { -AsFunction $AsFunction ` -Tags $Tags ` -ResourceNames $ResourceNames + + # Write-Output "ipamSuffix=$($deployment.Outputs["suffix"].Value)" >> $Env:GITHUB_OUTPUT } if ($PSCmdlet.ParameterSetName -eq 'Full') { @@ -1025,6 +1027,13 @@ process { finally { Write-Host Stop-Transcript | Out-Null + + Write-Output "ipamURL=https://$($deployment.Outputs["appServiceHostName"].Value)" >> $Env:GITHUB_OUTPUT + Write-Output "ipamUIAppId=$($appDetails.UIAppId)" >> $Env:GITHUB_OUTPUT + Write-Output "ipamEngineAppId=$($appDetails.EngineAppId)" >> $Env:GITHUB_OUTPUT + Write-Output "ipamSuffix=$($deployment.Outputs["suffix"].Value)" >> $Env:GITHUB_OUTPUT + Write-Output "ipamResourceGroup=$($deployment.Outputs["resourceGroupName"].Value)" >> $Env:GITHUB_OUTPUT + exit } } diff --git a/tests/azureipam.tests.ps1 b/tests/azureipam.tests.ps1 new file mode 100644 index 0000000..625e5ea --- /dev/null +++ b/tests/azureipam.tests.ps1 @@ -0,0 +1,454 @@ +BeforeAll { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + Set-StrictMode -Version Latest + + [string]$baseUrl = "$env:IPAM_URL/api" + [System.Security.SecureString]$accessToken = ConvertTo-SecureString (Get-AzAccessToken -ResourceUrl api://$env:IPAM_ENGINE_APP_ID).Token -AsPlainText + [hashtable]$headers = @{ + "Content-Type" = "application/json" + } + + # GET API Request + Function Get-ApiResource { + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$True, Position=0)] + [string]$resource, + + [Parameter(Mandatory=$False, Position=1)] + [hashtable]$query + ) + + $response = Invoke-RestMethod ` + -Method Get ` + -Authentication Bearer ` + -Token $accessToken ` + -Uri "${baseUrl}${resource}" ` + -Headers $headers ` + -Body $query ` + -StatusCodeVariable status + + Write-Output $response, $status + } + + # POST API Request + Function New-ApiResource { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$True, Position=0)] + [string]$resource, + + [Parameter(Mandatory=$True, Position=1)] + [hashtable]$body + ) + + $jsonBody = $body | ConvertTo-Json + $response = Invoke-RestMethod ` + -Method Post ` + -Authentication Bearer ` + -Token $accessToken ` + -Uri "${baseUrl}${resource}" ` + -Headers $headers ` + -Body $jsonBody ` + -StatusCodeVariable status + + Write-Output $response, $status + } + + # PUT API Request + Function Set-ApiResource { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$True, Position=0)] + [string]$resource, + + [Parameter(Mandatory=$True, Position=1)] + [object[]]$body + ) + + $jsonBody = $body | ConvertTo-Json + $response = Invoke-RestMethod ` + -Method Put ` + -Authentication Bearer ` + -Token $accessToken ` + -Uri "${baseUrl}${resource}" ` + -Headers $headers ` + -Body $jsonBody ` + -StatusCodeVariable status + + Write-Output $response, $status + } + + # PATCH API Request + Function Update-ApiResource { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$True, Position=0)] + [string]$resource, + + [Parameter(Mandatory=$True, Position=1)] + [hashtable[]]$body + ) + + $jsonBody = $body | ConvertTo-Json + $response = Invoke-RestMethod ` + -Method Patch ` + -Authentication Bearer ` + -Token $accessToken ` + -Uri "${baseUrl}${resource}" ` + -Headers $headers ` + -Body $jsonBody ` + -StatusCodeVariable status + + Write-Output $response, $status + } + + # DELETE API Request + Function Remove-ApiResource { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$True, Position=0)] + [string]$resource, + + [Parameter(Mandatory=$False, Position=1)] + [string[]]$body + ) + + $jsonBody = $body | ConvertTo-Json -AsArray + $response = Invoke-RestMethod ` + -Method Delete ` + -Authentication Bearer ` + -Token $accessToken ` + -Uri "${baseUrl}${resource}" ` + -Headers $headers ` + -Body $jsonBody ` + -StatusCodeVariable status + + Write-Output $response, $status + } +} + +Context 'Spaces' { + It 'Verify No Spaces Exist' { + + $spaces, $spacesStatus = Get-ApiResource '/spaces' + + $spaces | Should -Be $null + } + + It 'Create Two Spaces' { + $spaceA = @{ + name = 'TestSpace01' + desc = 'Test Space 1' + } + + $spaceB = @{ + name = 'TestSpace02' + desc = 'Test Space 2' + } + + New-ApiResource '/spaces' $spaceA + New-ApiResource '/spaces' $spaceB + + $spaces, $spacesStatus = Get-ApiResource '/spaces' + + $spaces.Count | Should -Be 2 + $spaces.Name -contains 'TestSpace01' | Should -Be $true + $spaces.Name -contains 'TestSpace02' | Should -Be $true + } + + It 'Delete a Space' { + Remove-ApiResource '/spaces/TestSpace02' + + $spaces, $spacesStatus = Get-ApiResource '/spaces' + + $spaces.Count | Should -Be 1 + $spaces.Name -contains 'TestSpace01' | Should -Be $true + $spaces.Name -contains 'TestSpace02' | Should -Be $false + } + + It 'Update a Space' { + $update = @( + @{ + op = 'replace' + path = '/name' + value = 'TestSpaceA' + } + @{ + op = 'replace' + path = '/desc' + value = 'Test Space A' + } + ) + + Update-ApiResource '/spaces/TestSpace01' $update + + $spaces, $spacesStatus = Get-ApiResource '/spaces' + + $spaces.Count | Should -Be 1 + $spaces[0].Name -eq 'TestSpaceA' | Should -Be $true + $spaces[0].Desc -eq 'Test Space A' | Should -Be $true + } +} + +Context 'Blocks' { + It 'Verify No Blocks Exist' { + + $blocks, $blocksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks' + + $blocks | Should -Be $null + } + + It 'Create Two Blocks' { + $blockA = @{ + name = 'TestBlock01' + cidr = '10.0.0.0/16' + } + + $blockB = @{ + name = 'TestBlock02' + cidr = '192.168.0.0/24' + } + + New-ApiResource '/spaces/TestSpaceA/blocks' $blockA + New-ApiResource '/spaces/TestSpaceA/blocks' $blockB + + $blocks, $blocksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks' + + $blocks.Count | Should -Be 2 + $blocks.Name -contains 'TestBlock01' | Should -Be $true + $blocks.Name -contains 'TestBlock02' | Should -Be $true + } + + It 'Delete a Block' { + Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlock02' + + $blocks, $blocksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks' + + $blocks.Count | Should -Be 1 + $blocks.Name -contains 'TestBlock01' | Should -Be $true + $blocks.Name -contains 'TestBlock02' | Should -Be $false + } + + It 'Update a Block' { + $update = @( + @{ + op = 'replace' + path = '/name' + value = 'TestBlockA' + } + @{ + op = 'replace' + path = '/cidr' + value = '10.1.0.0/16' + } + ) + + Update-ApiResource '/spaces/TestSpaceA/blocks/TestBlock01' $update + + $blocks, $blocksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks' + + $blocks.Count | Should -Be 1 + $blocks[0].Name -eq 'TestBlockA' | Should -Be $true + $blocks[0].Cidr -eq '10.1.0.0/16' | Should -Be $true + } +} + +Context 'Networks' { + It 'Verify No Networks Exist in Block' { + + $networks, $networksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/networks' + + $networks | Should -Be $null + } + + It 'Add a Virtual Network to a Block' { + $script:newNetA = New-AzVirtualNetwork ` + -Name 'TestVNet01' ` + -ResourceGroupName $env:IPAM_RESOURCE_GROUP ` + -Location 'westus3' ` + -AddressPrefix '10.1.0.0/24' + + Start-Sleep -Seconds 60 + + $body = @{ + id = $script:newNetA.Id + } + + New-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/networks' $body + + $block, $blockStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA' + + $($block.vnets | Select-Object -ExpandProperty id) -contains $script:newNetA.Id | Should -Be $true + } + + It 'Replace Block Virtual Networks' { + $script:newNetB = New-AzVirtualNetwork ` + -Name 'TestVNet02' ` + -ResourceGroupName $env:IPAM_RESOURCE_GROUP ` + -Location 'westus3' ` + -AddressPrefix '10.1.1.0/24' + + Start-Sleep -Seconds 60 + + $body = @( + $script:newNetA.Id + $script:newNetB.Id + ) + + Set-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/networks' $body + + $networks, $networksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/networks' + + $($networks | Select-Object -ExpandProperty id) -contains $script:newNetA.Id | Should -Be $true + $($networks | Select-Object -ExpandProperty id) -contains $script:newNetB.Id | Should -Be $true + } + + It 'Delete Block Virtual Network' { + $body = @( + $script:newNetB.Id + ) + + Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/networks' $body + + $networks, $networksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/networks' + + $($networks | Select-Object -ExpandProperty id) -contains $script:newNetA.Id | Should -Be $true + $($networks | Select-Object -ExpandProperty id) -contains $script:newNetB.Id | Should -Be $false + } +} + +Context 'External Networks' { + It 'Verify No External Networks Exist in Block' { + + $externals, $externalsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' + + $externals | Should -Be $null + } + + It 'Add an External Network to Block' { + $script:externalA = @{ + name = "ExternalNetA" + desc = "External Network A" + cidr = "10.1.1.0/24" + } + + New-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' $script:externalA + + $externals, $externalsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' + + $externals.Name -contains "ExternalNetA" | Should -Be $true + } + + It 'Replace Block External Networks' { + $script:externalB = @{ + name = "ExternalNetB" + desc = "External Network B" + cidr = "10.1.2.0/24" + } + + $script:externalC = @{ + name = "ExternalNetC" + desc = "External Network C" + cidr = "10.1.3.0/24" + } + + $body = @( + $script:externalA + $script:externalB + $script:externalC + ) + + Set-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' $body + + $externals, $externalsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' + + $externals.Name -contains "ExternalNetA" | Should -Be $true + $externals.Name -contains "ExternalNetB" | Should -Be $true + } + + It 'Delete Block External Network' { + $body = @( + $script:externalC.name + ) + + Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' $body + + $externals, $externalsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' + + $externals.Name -contains "ExternalNetA" | Should -Be $true + $externals.Name -contains "ExternalNetB" | Should -Be $true + $externals.Name -contains "ExternalNetC" | Should -Be $false + } + + It 'Get Specific Block External Network' { + + $external, $externalStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetB' + + $external.Name | Should -Be "ExternalNetB" + $external.Desc | Should -Be "External Network B" + $external.Cidr | Should -Be "10.1.2.0/24" + } + + It 'Delete Specific Block External Network' { + + Remove-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals/ExternalNetB' + + $externals, $externalsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/externals' + + $externals.Name -contains "ExternalNetA" | Should -Be $true + $externals.Name -contains "ExternalNetB" | Should -Be $false + $externals.Name -contains "ExternalNetC" | Should -Be $false + } +} + +Context 'Reservations' { + It 'Verify No Reservations Exist in Block' { + + $reservations, $reservationsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' + + $reservations | Should -Be $null + } + + It 'Create Block Reservation' { + $body = @{ + size = 24 + desc = "Test Reservation A" + } + + $script:reservationA, $reservationAStatus = New-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' $body + $reservations, $reservationsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' + + $reservations.Count | Should -Be 1 + $reservations[0].Space -eq "TestSpaceA" | Should -Be $true + $reservations[0].Block -eq "TestBlockA" | Should -Be $true + $reservations[0].Desc -eq "Test Reservation A" | Should -Be $true + $reservations[0].Cidr -eq "10.1.2.0/24" | Should -Be $true + $reservations[0].SettledOn -eq $null | Should -Be $true + } + + It 'Import Virtual Network via Reservation ID' { + $script:newNetC = New-AzVirtualNetwork ` + -Name 'TestVNet03' ` + -ResourceGroupName $env:IPAM_RESOURCE_GROUP ` + -Location 'westus3' ` + -AddressPrefix $script:reservationA.Cidr ` + -Tag @{ "X-IPAM-RES-ID" = $script:reservationA.Id } + + Start-Sleep -Seconds 180 + + $query = @{ + settled = $true + } + + $networks, $networksStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/networks' + $reservations, $reservationsStatus = Get-ApiResource '/spaces/TestSpaceA/blocks/TestBlockA/reservations' $query + + $($networks | Select-Object -ExpandProperty id) -contains $script:newNetA.Id | Should -Be $true + $($networks | Select-Object -ExpandProperty id) -contains $script:newNetC.Id | Should -Be $true + $reservations | Should -Not -Be $null + $reservations[0].SettledOn -eq $null | Should -Be $false + $reservations[0].Status -eq "fulfilled" | Should -Be $true + } +}