Tests/Export-MsecWordReport.Tests.ps1

#Requires -Module Pester
#
# Tests for Export-MsecWordReport. Verifies the shape-detection dispatch:
# - VMScript-shaped rows -> per-VM page with Courier New output.
# - Anything else -> a flat Word table with auto-detected columns.
# The whole file is skipped if PSWriteOffice isn't available (it's an optional
# dep, not declared in msec's RequiredModules).

BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..' 'Msec.psm1'
    Import-Module $modulePath -Force -ErrorAction Stop
}

AfterAll {
    Remove-Module Msec -Force -ErrorAction SilentlyContinue
}

Describe 'Export-MsecWordReport' -Skip:($null -eq (Get-Module -ListAvailable PSWriteOffice)) {
    It 'detects VMScript-shaped input and produces the per-VM page layout in Courier New' {
        $tmp = Join-Path ([System.IO.Path]::GetTempPath()) "msec-report-$([guid]::NewGuid().Guid).docx"
        $unzip = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().Guid)

        try {
            $rows = @(
                [pscustomobject]@{
                    VmName='lin-1'; ResourceGroupName='rg-a'; Os='Linux'
                    ScriptName='ntp-status'; Status='Succeeded'
                    Output="hostname: web-test`nNTP service: active"
                    Error=$null; DurationSeconds=31.6
                }
                [pscustomobject]@{
                    VmName='win-1'; ResourceGroupName='rg-a'; Os='Windows'
                    ScriptName='ntp-status'; Status='Succeeded'
                    Output='W32Time: Running'
                    Error=$null; DurationSeconds=12.4
                }
            )
            $result = $rows | Export-MsecWordReport `
                -Path $tmp -Title 'NTP evidence' -Subtitle 'ISO 27001 A.8.17'

            $result.Exists                | Should -BeTrue
            ($result.Length -gt 0)        | Should -BeTrue

            Expand-Archive -Path $tmp -DestinationPath $unzip
            $xml = Get-Content (Join-Path $unzip 'word/document.xml') -Raw
            $xml | Should -Match 'NTP evidence'
            $xml | Should -Match 'ISO 27001 A.8.17'
            $xml | Should -Match 'lin-1'
            $xml | Should -Match 'win-1'
            $xml | Should -Match 'hostname: web-test'
            $xml | Should -Match 'W32Time: Running'
            $xml | Should -Match 'Courier New'   # monospace styling applied to script output
        }
        finally {
            Remove-Item -LiteralPath $tmp   -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $unzip -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'falls back to a Word table for any other shape and includes every column + cell value' {
        $tmp = Join-Path ([System.IO.Path]::GetTempPath()) "msec-report-generic-$([guid]::NewGuid().Guid).docx"
        $unzip = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().Guid)

        try {
            # Deliberately NOT VMScript-shaped: no VmName/ScriptName/Output. This is
            # the kind of row you'd get from Get-MsecSecureScore or a hand-built
            # tabular result.
            $rows = @(
                [pscustomobject]@{ Date='2026-04-01'; Score=72; Diff=+3 }
                [pscustomobject]@{ Date='2026-05-01'; Score=78; Diff=+6 }
                [pscustomobject]@{ Date='2026-06-01'; Score=81; Diff=+3 }
            )
            $result = $rows | Export-MsecWordReport -Path $tmp -Title 'Secure Score Trend'

            $result.Exists         | Should -BeTrue
            ($result.Length -gt 0) | Should -BeTrue

            Expand-Archive -Path $tmp -DestinationPath $unzip
            $xml = Get-Content (Join-Path $unzip 'word/document.xml') -Raw

            # Cover-page strings.
            $xml | Should -Match 'Secure Score Trend'
            $xml | Should -Match 'Total rows: 3'

            # Headers + a sample of cell values from every row prove the table contains
            # the data (rather than e.g. silently dropping rows when not VMScript-shaped).
            $xml | Should -Match 'Date'
            $xml | Should -Match 'Score'
            $xml | Should -Match 'Diff'
            $xml | Should -Match '2026-04-01'
            $xml | Should -Match '2026-05-01'
            $xml | Should -Match '2026-06-01'
            $xml | Should -Match '<w:t[^>]*>72</w:t>'
            $xml | Should -Match '<w:t[^>]*>81</w:t>'

            # And specifically NOT the VMScript layout - no Courier-styled paragraphs
            # leaked through.
            $xml | Should -Not -Match 'Courier New'
        }
        finally {
            Remove-Item -LiteralPath $tmp   -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $unzip -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'emits a warning and produces no file when nothing is piped' {
        $tmp = Join-Path ([System.IO.Path]::GetTempPath()) "msec-report-empty-$([guid]::NewGuid().Guid).docx"
        @() | Export-MsecWordReport -Path $tmp -WarningAction SilentlyContinue
        (Test-Path -LiteralPath $tmp) | Should -BeFalse
    }
}