tests/Test-PptxFromDataDump.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Test PPTX executive-deck generation from a previous data dump — no Azure connection required. .DESCRIPTION Mirrors the Test-ExcelFromDataDump.ps1 / Test-PowerBIFromDataDump.ps1 pattern: reads the synthetic JSON data dump from tests/datadump/, but since Export-Pptx (src/report/renderers/Export-Pptx.ps1) consumes the scored Findings contract produced by Get-Score (GeneratedOn/Frameworks/ Areas/Gaps/Manual/Errors/Findings) rather than the raw ARM-inventory shape sample-report.json carries (that shape belongs to the legacy Modules/Public/InventoryModules Excel/Power BI pipeline), this harness synthesizes a plausible Findings array from sample-report.json's ARM category/module inventory — deterministic Pass/Partial/Fail/Manual/ Unknown statuses across CAF and WAF, including deliberately blank/null severities on some Fail rows so the AB#5089 defensive sort/label guard (null/unknown severity sorts LAST, never throws) is actually exercised end-to-end through Get-Score -> Export-Pptx. This lets you iterate on deck layout/branding without a live Azure scan or a real assessment run every time. .PARAMETER DataDumpPath Path to the data dump folder containing the JSON report. Defaults to tests/datadump/ relative to the repo root. .PARAMETER OutputPath Where to write the generated deck. Defaults to tests/test-output/. .EXAMPLE # From the repo root: .\tests\Test-PptxFromDataDump.ps1 .NOTES Requires: DocumentFormat.OpenXml (acquired on first use via dotnet/NuGet by Export-Pptx.ps1 itself — see Import-ScoutOpenXmlAssembly). No Az.* modules, no ImportExcel — this harness dot-sources only the assessment engine + report renderer files it needs, so it never triggers AzureScout.psm1's Install-Module bootstrap for unrelated dependencies. Version: 1.0.0 #> [CmdletBinding()] param( [string]$DataDumpPath, [string]$OutputPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # ── Resolve Paths ──────────────────────────────────────────────────────── $RepoRoot = Split-Path $PSScriptRoot -Parent if (-not $DataDumpPath) { $DataDumpPath = Join-Path $RepoRoot 'tests' 'datadump' } if (-not $OutputPath) { $OutputPath = Join-Path $RepoRoot 'tests' 'test-output' } Write-Host "`n═══════════════════════════════════════════════════════════" -ForegroundColor DarkCyan Write-Host " AzureScout — PPTX Report Test Harness" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════════════════════`n" -ForegroundColor DarkCyan # Find the JSON report file — prefer the synthetic sample if present $SampleFile = Join-Path $DataDumpPath 'sample-report.json' if (Test-Path $SampleFile) { $JsonFile = Get-Item $SampleFile } else { $JsonFile = Get-ChildItem -Path $DataDumpPath -Filter '*.json' -ErrorAction SilentlyContinue | Select-Object -First 1 } if (-not $JsonFile) { Write-Error "No JSON report file found in $DataDumpPath. Run .\tests\New-SyntheticSampleReport.ps1 first." return } Write-Host " Source JSON: " -NoNewline -ForegroundColor Gray Write-Host $JsonFile.Name -ForegroundColor White # ── Load only what Export-Pptx needs (no Az.*/ImportExcel bootstrap) ──── Write-Host " Loading assessment engine + renderer..." -ForegroundColor Gray . (Join-Path $RepoRoot 'src' 'assess' 'engine' 'Get-Score.ps1') . (Join-Path $RepoRoot 'src' 'report' 'renderers' 'Export-Pptx.ps1') # ── Parse the JSON Report ──────────────────────────────────────────────── Write-Host " Parsing JSON report..." -ForegroundColor Gray $ReportData = Get-Content $JsonFile.FullName -Raw | ConvertFrom-Json -Depth 40 $Metadata = $ReportData._metadata Write-Host " Tenant: " -NoNewline -ForegroundColor Gray Write-Host ($Metadata.tenantId ?? 'N/A') -ForegroundColor White Write-Host " Generated: " -NoNewline -ForegroundColor Gray Write-Host ($Metadata.generatedAt ?? 'N/A') -ForegroundColor White Write-Host " Scope: " -NoNewline -ForegroundColor Gray Write-Host ($Metadata.scope ?? 'N/A') -ForegroundColor White # ── Synthesize a scored Findings object from the ARM category inventory ─ Write-Host "`n Synthesizing findings from ARM category/module inventory..." -ForegroundColor Gray $statuses = @('Pass', 'Pass', 'Partial', 'Fail', 'Manual', 'Unknown', 'Fail') # Severities deliberately include $null and '' so the AB#5089 guard on the # null/unknown-severity gap sort is exercised, not just the recognized values. $severities = @('high', 'medium', 'low', $null, '', 'bogus-severity') $findings = [System.Collections.Generic.List[object]]::new() $i = 0 if ($ReportData.arm) { foreach ($categoryProp in $ReportData.arm.PSObject.Properties) { $framework = if (($i % 2) -eq 0) { 'CAF' } else { 'WAF' } $area = $categoryProp.Name.Substring(0, 1).ToUpper() + $categoryProp.Name.Substring(1) foreach ($moduleProp in $categoryProp.Value.PSObject.Properties) { $i++ $status = $statuses[$i % $statuses.Count] $severity = if ($status -eq 'Fail') { $severities[$i % $severities.Count] } else { 'medium' } $findings.Add([pscustomobject]@{ Id = "$($categoryProp.Name)-$($moduleProp.Name)" Title = "$($moduleProp.Name) compliance check" Framework = $framework Area = $area Severity = $severity Status = $status EvidenceCount = @($moduleProp.Value).Count Evidence = @() Remediation = "Review $($moduleProp.Name) configuration against the $framework baseline." Manual = ($status -eq 'Manual') AreaWeight = 1.0 }) } } } if ($findings.Count -eq 0) { Write-Error "sample-report.json produced zero synthetic findings — check its 'arm' section shape." return } Write-Host " Synthetic findings: $($findings.Count) across $(@($findings.Framework | Select-Object -Unique).Count) framework(s), $(@($findings.Area | Select-Object -Unique).Count) area(s)" -ForegroundColor Green $scored = Get-Score -Findings $findings Write-Host " Scored — Frameworks: $($scored.Frameworks.Count), Areas: $($scored.Areas.Count), Gaps: $($scored.Gaps.Count), Manual: $($scored.Manual.Count)" -ForegroundColor Green # A minimal Collect-shaped object — Export-Pptx only reads _meta.scope / # _meta.managementGroupId from it (Collect carries no tenant field in the # real src/collect/Invoke-Collect.ps1 contract either). $collect = [pscustomobject]@{ _meta = [pscustomobject]@{ generatedOn = $Metadata.generatedAt scope = $Metadata.scope managementGroupId = $Metadata.tenantId } } # ── Prepare Output Folder ──────────────────────────────────────────────── if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } # ── Render the deck ────────────────────────────────────────────────────── Write-Host "`n── Rendering PPTX ───────────────────────────────────────────" -ForegroundColor DarkCyan $ValidationErrors = 0 $DeckFile = $null $RenderTimer = [System.Diagnostics.Stopwatch]::StartNew() try { $DeckFile = Export-Pptx -Findings $scored -Collect $collect -OutputPath $OutputPath } catch { $RenderTimer.Stop() Write-Host " [FAIL] Export-Pptx threw — deck was not rendered." -ForegroundColor Red Write-Host " $_" -ForegroundColor Red Write-Host "`n═══════════════════════════════════════════════════════════`n" -ForegroundColor DarkCyan throw } $RenderTimer.Stop() Write-Host " Render time: " -NoNewline -ForegroundColor Green Write-Host $RenderTimer.Elapsed.ToString("mm\:ss\.fff") -ForegroundColor Cyan Write-Host " Output file: " -NoNewline -ForegroundColor Gray Write-Host $DeckFile -ForegroundColor White # ── Validate the rendered deck ─────────────────────────────────────────── Write-Host "`n── Validation ───────────────────────────────────────────────" -ForegroundColor DarkCyan if (Test-Path $DeckFile) { $fileInfo = Get-Item $DeckFile Write-Host " [OK] File exists" -ForegroundColor Green Write-Host " [OK] Size: $([math]::Round($fileInfo.Length / 1KB, 1)) KB" -ForegroundColor Green } else { Write-Host " [FAIL] Deck file was not created: $DeckFile" -ForegroundColor Red $ValidationErrors++ } $expectedMinSlides = 6 # title + summary + >=1 area page + >=1 gaps page + manual + next-steps if (Test-Path $DeckFile) { try { Add-Type -AssemblyName System.IO.Compression -ErrorAction SilentlyContinue $zipBytes = [System.IO.File]::ReadAllBytes($DeckFile) $ms = [System.IO.MemoryStream]::new($zipBytes) $zip = [System.IO.Compression.ZipArchive]::new($ms, [System.IO.Compression.ZipArchiveMode]::Read) # Theme part URI depends on which part it was added under (slideMaster vs. # presentation) — both are valid OPC placements, so match by filename pattern # rather than a hardcoded path. $required = @('[Content_Types].xml', 'ppt/presentation.xml', 'ppt/slideMasters/slideMaster1.xml') $entries = @($zip.Entries | Select-Object -ExpandProperty FullName) $missing = @($required | Where-Object { $_ -notin $entries }) $hasTheme = @($entries | Where-Object { $_ -match 'theme\d*\.xml$' }).Count -gt 0 if (-not $hasTheme) { $missing += 'theme*.xml (any location)' } if ($missing.Count -eq 0) { Write-Host " [OK] Valid OPC/zip package — required parts present ($($entries.Count) entries total)" -ForegroundColor Green } else { Write-Host " [FAIL] Missing required OPC parts: $($missing -join ', ')" -ForegroundColor Red $ValidationErrors++ } $slideEntries = @($entries | Where-Object { $_ -match '^ppt/slides/slide\d+\.xml$' }) Write-Host " [OK] Slide count: $($slideEntries.Count)" -ForegroundColor Green if ($slideEntries.Count -lt $expectedMinSlides) { Write-Host " [FAIL] Expected at least $expectedMinSlides slides (title, summary, area, gaps, manual, next-steps), found $($slideEntries.Count)" -ForegroundColor Red $ValidationErrors++ } $zip.Dispose() $ms.Dispose() } catch { Write-Host " [FAIL] Could not read the deck as a zip/OPC package: $_" -ForegroundColor Red $ValidationErrors++ } # Full schema validation via the OpenXML SDK's own validator — this is the # strongest signal the package is actually well-formed PresentationML, not # just a zip with the right file names. try { $doc2 = [DocumentFormat.OpenXml.Packaging.PresentationDocument]::Open($DeckFile, $false) $validator = New-Object DocumentFormat.OpenXml.Validation.OpenXmlValidator $issues = @($validator.Validate($doc2)) if ($issues.Count -eq 0) { Write-Host " [OK] OpenXmlValidator: 0 schema issues" -ForegroundColor Green } else { Write-Host " [FAIL] OpenXmlValidator found $($issues.Count) schema issue(s):" -ForegroundColor Red $issues | Select-Object -First 10 | ForEach-Object { Write-Host " $($_.Description) [$($_.Part.Uri)]" -ForegroundColor Yellow } $ValidationErrors++ } $doc2.Dispose() } catch { Write-Host " [FAIL] OpenXmlValidator could not open the deck: $_" -ForegroundColor Red $ValidationErrors++ } } # ── Final Summary ──────────────────────────────────────────────────────── Write-Host "`n═══════════════════════════════════════════════════════════" -ForegroundColor DarkCyan if ($ValidationErrors -eq 0) { Write-Host " SUCCESS — PPTX deck generated and validated" -ForegroundColor Green } else { Write-Host " FAILED — $ValidationErrors validation error(s)" -ForegroundColor Red } Write-Host " File: $DeckFile" -ForegroundColor White Write-Host "═══════════════════════════════════════════════════════════`n" -ForegroundColor DarkCyan if ($ValidationErrors -gt 0) { exit 1 } |