Examples/Invoke-JuribaAppRSelfServiceWithTesting.ps1

<#
.SYNOPSIS
  Interactive self-service packaging with smoke-test visibility for Juriba App Readiness.
.DESCRIPTION
  PowerShell replacement for the appr-self-service-with-testing-visibility
  HTML portal. Runs the full self-service workflow (KB search or local file
  upload, upload, create, watch packaging) and - if automatic smoke testing
  is enabled in Default Settings - continues watching per-package ACE
  (Automated Compatibility Engine) test results until they complete.

  Per-format test results are reported as a table: format (MSI / MSIX /
  IntuneWin / ...), RAG status (Pass / Warn / Fail), progress, and time taken.
.PARAMETER InstanceUrl
  Base URL of the App Readiness instance. Not required if already connected
  via Connect-JuribaAppR.
.PARAMETER APIKey
  API key. Not required if already connected.
.PARAMETER SearchTerm
  Non-interactive: search the KB for this term and auto-select the top result
  and latest version/x64-preferred source.
.PARAMETER SetupFilePath
  Non-interactive: skip the KB, upload this local installer directly.
.PARAMETER IntervalSeconds
  Packaging status poll interval. Default 60.
.PARAMETER TimeoutMinutes
  Packaging status timeout. Default 60.
.PARAMETER TestTimeoutMinutes
  Smoke-test poll timeout once packaging completes. Default 60.
.PARAMETER TestIntervalSeconds
  Smoke-test poll interval. Default 20.
.PARAMETER SkipTestVisibility
  When specified, stops after packaging even if auto smoke testing is enabled.
.EXAMPLE
  .\Invoke-JuribaAppRSelfServiceWithTesting.ps1 -InstanceUrl "https://sandbox.appr.juriba.app" -APIKey $key
  Fully interactive mode.
.EXAMPLE
  .\Invoke-JuribaAppRSelfServiceWithTesting.ps1 -InstanceUrl "https://sandbox.appr.juriba.app" -APIKey $key -SearchTerm "7-Zip"
  Non-interactive KB run with post-packaging smoke-test monitoring.
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
    Justification = 'Interactive example script - user-facing colored console output for progress and results.')]
[CmdletBinding(DefaultParameterSetName = 'Interactive')]
param (
    [Parameter(Mandatory = $false)]
    [string]$InstanceUrl,

    [Parameter(Mandatory = $false)]
    [string]$APIKey,

    [Parameter(ParameterSetName = 'KB')]
    [string]$SearchTerm,

    [Parameter(ParameterSetName = 'Local')]
    [string]$SetupFilePath,

    [int]$IntervalSeconds     = 60,
    [int]$TimeoutMinutes      = 60,
    [int]$TestIntervalSeconds = 20,
    [int]$TestTimeoutMinutes  = 60,

    [switch]$SkipTestVisibility
)

$ErrorActionPreference = 'Stop'

# --- Import module -----------------------------------------------------------
$modulePath = Join-Path (Join-Path $PSScriptRoot '..') 'Juriba.AppR.psd1'
if (-not (Test-Path $modulePath)) {
    Write-Error "Cannot find Juriba.AppR.psd1 at $modulePath. Run this script from its own folder."
}
Import-Module $modulePath -Force
Write-Host "Juriba.AppR module imported" -ForegroundColor Cyan

# --- Connect -----------------------------------------------------------------
$connectedHere  = $false
$existing       = Get-JuribaAppRSession
$activeInstance = $null
if ($existing -and -not $InstanceUrl) {
    $activeInstance = $existing.Instance
    Write-Host "Using existing session: $activeInstance" -ForegroundColor Green
}
else {
    if (-not $InstanceUrl) { $InstanceUrl = Read-Host "Instance URL (e.g. https://sandbox.appr.juriba.app)" }
    if (-not $APIKey)      { $APIKey      = Read-Host "API key" -MaskInput }
    Connect-JuribaAppR -Instance $InstanceUrl -APIKey $APIKey
    $activeInstance = $InstanceUrl
    Write-Host "Connected to $activeInstance" -ForegroundColor Green
    $connectedHere  = $true
}

# --- Helpers -----------------------------------------------------------------
# Package type labels (server TypeOfPackage enum, matching the HTML portal)
$pkgLabels = @{
    0  = 'MSI'
    1  = 'App-V'
    2  = 'MSIX'
    3  = 'MSIX'
    4  = 'IntuneWin'
    5  = 'MSIX App Attach'
    6  = 'PSADT'
    -1 = 'Source'
}

function Get-TestState {
    <#
    Returns @{ Status = string; Color = ConsoleColor; Done = bool; Progress = int }
    from an ACE evergreenInformation entry.
    #>

    param($EgEntry)

    if (-not $EgEntry) { return @{ Status = 'Pending'; Color = 'DarkGray'; Done = $false; Progress = 0 } }
    $t = $EgEntry[0]
    if ($t.complete -and $t.rAGStatus -eq 1) { return @{ Status = 'Passed';                Color = 'Green';     Done = $true;  Progress = 100 } }
    if ($t.complete -and $t.rAGStatus -eq 2) { return @{ Status = 'Passed with warnings';  Color = 'Yellow';    Done = $true;  Progress = 100 } }
    if ($t.complete -and ($t.rAGStatus -eq 3 -or $t.statusId -eq 100)) {
                                               return @{ Status = 'Failed';                Color = 'Red';       Done = $true;  Progress = ($t.progress ?? 0) } }
    if ($t.progress -gt 0)                   { return @{ Status = "Testing $($t.progress)%"; Color = 'Cyan';   Done = $false; Progress = $t.progress } }
    return @{ Status = 'Queued'; Color = 'DarkGray'; Done = $false; Progress = 0 }
}

function Watch-SmokeTest {
    param (
        [int]$AppId,
        [int]$IntervalSeconds,
        [int]$TimeoutMinutes
    )

    $deadline = (Get-Date).AddMinutes($TimeoutMinutes)
    $lastSummary = $null
    $finalEntries = $null

    while ((Get-Date) -lt $deadline) {
        try {
            $tests = @(Get-JuribaAppRTestApplication -AppId $AppId)
        }
        catch {
            Write-Host " Test poll error: $($_.Exception.Message)" -ForegroundColor DarkGray
            Start-Sleep -Seconds $IntervalSeconds
            continue
        }

        $entries = @($tests | Where-Object { $_.typeOfPackage -ge 0 })
        if (-not $entries.Count) {
            Start-Sleep -Seconds $IntervalSeconds
            continue
        }

        $summary = foreach ($t in $entries) {
            $format = $script:pkgLabels[[int]$t.typeOfPackage]
            if (-not $format) { $format = "Type $($t.typeOfPackage)" }
            $eg = $t.evergreenInformation
            $st = Get-TestState -EgEntry $eg
            $vm = if ($eg) { $eg[0].virtualMachineName } else { $null }
            $tt = if ($eg) { $eg[0].testingTimeTakenString } else { $null }
            [PSCustomObject]@{
                Format  = $format
                Status  = $st.Status
                Done    = $st.Done
                Color   = $st.Color
                VM      = $vm
                Time    = $tt
                Entry   = $t
            }
        }

        $summaryKey = ($summary | ForEach-Object { "$($_.Format)=$($_.Status)" }) -join ';'
        if ($summaryKey -ne $lastSummary) {
            Write-Host ""
            Write-Host ("{0,-18} {1,-30} {2,-30} {3}" -f 'Format', 'Status', 'VM', 'Time')
            Write-Host ("{0,-18} {1,-30} {2,-30} {3}" -f ('-' * 18), ('-' * 30), ('-' * 30), ('-' * 10))
            foreach ($row in $summary) {
                $line = "{0,-18} {1,-30} {2,-30} {3}" -f $row.Format, $row.Status, ($row.VM ?? ''), ($row.Time ?? '')
                Write-Host $line -ForegroundColor $row.Color
            }
            $lastSummary = $summaryKey
        }

        $finalEntries = $summary
        if (($summary | Where-Object { -not $_.Done }).Count -eq 0) {
            Write-Host "`nAll smoke tests complete." -ForegroundColor Green
            return $finalEntries
        }

        Start-Sleep -Seconds $IntervalSeconds
    }

    Write-Warning "Smoke-test watch timed out after $TimeoutMinutes minutes."
    return $finalEntries
}

try {
    # --- Detect autoSmokeTest from Default Settings --------------------------
    # Default-setting type 8 with value=true indicates auto smoke testing.
    $autoSmokeTest = $false
    try {
        $settings = Get-JuribaAppRDefaultSetting
        if ($settings.Raw) {
            $smokeSetting = $settings.Raw | Where-Object { [int]$_.defaultSettingType -eq 8 } | Select-Object -First 1
            if ($smokeSetting -and $smokeSetting.value -eq 'true') { $autoSmokeTest = $true }
        }
    }
    catch {
        Write-Warning "Could not read Default Settings: $($_.Exception.Message)"
    }
    Write-Host ("Auto smoke testing: {0}" -f ($(if ($autoSmokeTest) { 'enabled' } else { 'disabled' }))) -ForegroundColor Cyan

    # --- Choose source -------------------------------------------------------
    $mode = switch ($PSCmdlet.ParameterSetName) {
        'KB'    { 'kb' }
        'Local' { 'local' }
        default {
            Write-Host ""
            Write-Host "Choose a source:" -ForegroundColor Cyan
            Write-Host " [1] Search the Knowledge Base"
            Write-Host " [2] Upload a local installer"
            $choice = Read-Host "Select (1-2)"
            if ($choice -eq '2') { 'local' } else { 'kb' }
        }
    }

    $localPath = $null
    $metaHint  = $null
    $cleanupTempDownload = $false

    # --- KB source -----------------------------------------------------------
    if ($mode -eq 'kb') {
        if (-not $SearchTerm) { $SearchTerm = Read-Host "Search term" }

        Write-Host "`n=== Searching Knowledge Base for '$SearchTerm' ===" -ForegroundColor Magenta
        $kbResults = @(Search-JuribaAppRKnowledgeBase -Search $SearchTerm)
        if (-not $kbResults.Count) { throw "No Knowledge Base matches for '$SearchTerm'." }

        Write-Host "Found $($kbResults.Count) application(s):"
        for ($i = 0; $i -lt $kbResults.Count; $i++) {
            Write-Host (" [{0}] {1} ({2})" -f ($i + 1), $kbResults[$i].applicationName, $kbResults[$i].vendorName)
        }
        $appChoice = if ($kbResults.Count -eq 1 -or $PSCmdlet.ParameterSetName -eq 'KB') {
            $kbResults[0]
        }
        else {
            $idx = [int](Read-Host "Select application number (1-$($kbResults.Count))")
            $kbResults[$idx - 1]
        }
        Write-Host "Selected: $($appChoice.applicationName)" -ForegroundColor Green

        Write-Host "`n=== Fetching sources ===" -ForegroundColor Magenta
        $sources = @(Search-JuribaAppRKnowledgeBase -ApplicationId $appChoice.applicationId)
        if (-not $sources.Count) { throw "No sources found for '$($appChoice.applicationName)'." }

        $uniqueVersions = $sources |
            Select-Object -ExpandProperty version -Unique |
            Sort-Object { try { [version]($_ -replace '[^0-9.]', '') } catch { [version]'0.0' } } -Descending
        $topVersions = @($uniqueVersions | Select-Object -First 5)

        $verChoice = if ($topVersions.Count -eq 1 -or $PSCmdlet.ParameterSetName -eq 'KB') {
            $topVersions[0]
        }
        else {
            Write-Host "Latest versions:"
            for ($i = 0; $i -lt $topVersions.Count; $i++) {
                Write-Host (" [{0}] {1}" -f ($i + 1), $topVersions[$i])
            }
            $idx = Read-Host "Select version (1-$($topVersions.Count)) [default: 1 = latest]"
            if (-not $idx) { $idx = 1 }
            $topVersions[[int]$idx - 1]
        }
        Write-Host "Version: $verChoice" -ForegroundColor Green

        $versionSources = @($sources | Where-Object { $_.version -eq $verChoice })
        $srcChoice = if ($versionSources.Count -eq 1 -or $PSCmdlet.ParameterSetName -eq 'KB') {
            $x64Exe = $versionSources | Where-Object { $_.architecture -eq 64 -and $_.fileName -match '\.exe$' } | Select-Object -First 1
            if ($x64Exe) { $x64Exe } else { $versionSources[0] }
        }
        else {
            Write-Host "Sources for v${verChoice}:"
            for ($i = 0; $i -lt $versionSources.Count; $i++) {
                $arch = switch ($versionSources[$i].architecture) { 32 { 'x86' } 64 { 'x64' } default { "arch$($versionSources[$i].architecture)" } }
                Write-Host (" [{0}] {1} ({2})" -f ($i + 1), $versionSources[$i].fileName, $arch)
            }
            $idx = Read-Host "Select source (1-$($versionSources.Count))"
            $versionSources[[int]$idx - 1]
        }
        Write-Host "Source: $($srcChoice.fileName)" -ForegroundColor Green

        Write-Host "`n=== Downloading $($srcChoice.fileName) ===" -ForegroundColor Magenta
        $downloadDir = Join-Path ([System.IO.Path]::GetTempPath()) "JuribaAppR-SelfService"
        if (-not (Test-Path $downloadDir)) { New-Item -ItemType Directory -Path $downloadDir | Out-Null }
        $localPath = Join-Path $downloadDir $srcChoice.fileName

        if (Test-Path $localPath) {
            Write-Host "Already cached: $localPath" -ForegroundColor DarkGray
        }
        else {
            Invoke-WebRequest -Uri $srcChoice.downloadUrl -OutFile $localPath -UseBasicParsing
            Write-Host ("Downloaded {0:N2} MB to {1}" -f ((Get-Item $localPath).Length / 1MB), $localPath) -ForegroundColor Green
            $cleanupTempDownload = $true
        }

        # `??` only fires on $null. Some KB entries return an empty string for
        # productName/vendor — use a truthy check so "" falls back to the
        # application-level name/vendor instead of submitting a blank field.
        $metaHint = @{
            Name         = if ($srcChoice.productName) { $srcChoice.productName } else { $appChoice.applicationName }
            Manufacturer = if ($srcChoice.vendor)      { $srcChoice.vendor }      else { $appChoice.vendorName }
            Version      = $srcChoice.version
        }
    }
    # --- Local source --------------------------------------------------------
    else {
        if (-not $SetupFilePath) { $SetupFilePath = Read-Host "Path to installer" }
        if (-not (Test-Path $SetupFilePath -PathType Leaf)) { throw "File not found: $SetupFilePath" }
        $localPath = (Resolve-Path $SetupFilePath).Path
    }

    # --- Upload --------------------------------------------------------------
    Write-Host "`n=== Uploading ===" -ForegroundColor Magenta
    $upload = Send-JuribaAppRSetupFile -FilePath $localPath -Verbose:$VerbosePreference
    Write-Host ("Uploaded: {0} ({1:N2} MB, {2} chunks, UUID={3})" -f $upload.FileName, ($upload.FileSize / 1MB), $upload.TotalChunks, $upload.Uuid) -ForegroundColor Green

    # --- Create application --------------------------------------------------
    Write-Host "`n=== Creating application ===" -ForegroundColor Magenta
    $splat = @{
        Uuid           = $upload.Uuid
        FileName       = $upload.FileName
        FileSize       = $upload.FileSize
        TotalChunks    = $upload.TotalChunks
        RunImmediately = $true
    }
    if ($metaHint) {
        if ($metaHint.Name)         { $splat['Name']         = $metaHint.Name }
        if ($metaHint.Manufacturer) { $splat['Manufacturer'] = $metaHint.Manufacturer }
        if ($metaHint.Version)      { $splat['Version']      = $metaHint.Version }
    }
    else {
        if ($upload.Name)         { $splat['Name']         = $upload.Name }
        if ($upload.Manufacturer) { $splat['Manufacturer'] = $upload.Manufacturer }
        if ($upload.Version)      { $splat['Version']      = $upload.Version }
    }
    New-JuribaAppRApplication @splat -Verbose:$VerbosePreference | Out-Null
    Write-Host "Application submitted" -ForegroundColor Green

    # --- Wait for application ID ---------------------------------------------
    Write-Host "`n=== Waiting for application ID ===" -ForegroundColor Magenta
    $appId = $null
    $creationDeadline = (Get-Date).AddMinutes(5)
    while ((Get-Date) -lt $creationDeadline) {
        try {
            $state = Get-JuribaAppRApplicationCreationState -UploadId $upload.Uuid
            $resolved = if ($state.data.applicationId) { $state.data.applicationId }
                        elseif ($state.applicationId)   { $state.applicationId }
                        else { $null }
            if ($resolved -and $resolved -gt 0) { $appId = $resolved; break }
        }
        catch { Write-Verbose "Creation-state poll failed: $($_.Exception.Message)" }
        Write-Host " waiting..." -ForegroundColor DarkGray
        Start-Sleep -Seconds 10
    }
    if (-not $appId) { throw "Timed out waiting for the application ID." }
    Write-Host "Application ID: $appId" -ForegroundColor Green
    Write-Host ("View in App Readiness: {0}/applications/fullStatus/{1}" -f $activeInstance, $appId) -ForegroundColor Cyan

    # --- Watch packaging -----------------------------------------------------
    Write-Host "`n=== Watching packaging (Step 2 of 3) ===" -ForegroundColor Magenta
    $pkgResult = Watch-JuribaAppRApplicationStatus -AppId $appId `
        -IntervalSeconds $IntervalSeconds -TimeoutMinutes $TimeoutMinutes
    $packagingSucceeded = $pkgResult.Status -notmatch 'Fail|Cancel|Timeout'

    # --- Watch smoke tests ---------------------------------------------------
    $testResults = $null
    if ($packagingSucceeded -and $autoSmokeTest -and -not $SkipTestVisibility) {
        Write-Host "`n=== Watching smoke tests (Step 3 of 3) ===" -ForegroundColor Magenta
        $testResults = Watch-SmokeTest -AppId $appId `
            -IntervalSeconds $TestIntervalSeconds -TimeoutMinutes $TestTimeoutMinutes
    }
    elseif ($packagingSucceeded -and -not $autoSmokeTest) {
        Write-Host "`n(Auto smoke testing is not enabled on this instance; skipping Step 3.)" -ForegroundColor DarkGray
    }

    # --- Result summary ------------------------------------------------------
    Write-Host "`n=== Result ===" -ForegroundColor Magenta
    $statusColor = if ($packagingSucceeded) { 'Green' } else { 'Red' }
    Write-Host ("Packaging: {0} ({1}%)" -f $pkgResult.Status, $pkgResult.Progress) -ForegroundColor $statusColor
    if ($testResults) {
        Write-Host "Smoke tests:"
        foreach ($row in $testResults) {
            Write-Host (" {0,-18} {1,-30} {2}" -f $row.Format, $row.Status, ($row.Time ?? '')) -ForegroundColor $row.Color
        }
    }
    Write-Host ("URL: {0}/applications/fullStatus/{1}" -f $activeInstance, $appId) -ForegroundColor Cyan

    if (-not $packagingSucceeded) {
        Write-Host "`nEvent history:" -ForegroundColor Yellow
        try {
            $events = Get-JuribaAppRApplicationEvent -AppId $appId
            $events | ForEach-Object {
                $ts  = $_.date ?? $_.timestamp ?? ''
                $msg = $_.message ?? $_.description ?? ($_ | ConvertTo-Json -Compress)
                Write-Host " [$ts] $msg"
            }
        }
        catch { Write-Host " (could not retrieve events: $($_.Exception.Message))" -ForegroundColor DarkGray }
    }

    [PSCustomObject]@{
        AppId            = $appId
        PackagingStatus  = $pkgResult.Status
        PackagingResult  = $pkgResult
        AutoSmokeTest    = $autoSmokeTest
        TestResults      = $testResults
    }
}
finally {
    if ($cleanupTempDownload -and $localPath -and (Test-Path $localPath)) {
        Remove-Item $localPath -Force -ErrorAction SilentlyContinue
    }
    if ($connectedHere) { Disconnect-JuribaAppR }
}