Public/Tests/Export-ModuleToFlatTextAndManifest.Tests.ps1

BeforeAll {
    $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config'
    $env:TCS_SKIP_UPDATE_CHECK = '1'
    $env:TCS_TELEMETRY_OPTOUT = '1'
    $ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
    Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.utils.psd1') -Force
}

AfterAll {
    Remove-Module -Name tcs.utils -Force -ErrorAction SilentlyContinue
}

Describe 'Export-ModuleToFlatTextAndManifest' {
    BeforeAll {
        $ModuleSource = Join-Path -Path $TestDrive -ChildPath 'src/my.module'
        $null = New-Item -Path (Join-Path -Path $ModuleSource -ChildPath 'Public/Tests') -ItemType Directory -Force
        Set-Content -Path (Join-Path -Path $ModuleSource -ChildPath 'my.module.psd1') -Value "@{ ModuleVersion = '1.0.0' }`n" -NoNewline
        Set-Content -Path (Join-Path -Path $ModuleSource -ChildPath 'Public/Get-Thing.ps1') -Value "function Get-Thing {`n 'thing'`n}`n" -NoNewline
        Set-Content -Path (Join-Path -Path $ModuleSource -ChildPath 'no-newline.txt') -Value 'last line' -NoNewline
        [System.IO.File]::WriteAllBytes((Join-Path -Path $ModuleSource -ChildPath 'icon.png'), [byte[]](0x89, 0x00, 0x01))
        $ExportPath = Join-Path -Path $TestDrive -ChildPath 'export'
        Export-ModuleToFlatTextAndManifest -ModulePath $ModuleSource -OutputPath $ExportPath -WarningVariable exportWarnings -WarningAction SilentlyContinue
        $Manifest = Get-Content -Path (Join-Path -Path $ExportPath -ChildPath 'my.module_manifest.json') -Raw | ConvertFrom-Json
        $FlatText = Get-Content -Path (Join-Path -Path $ExportPath -ChildPath 'my.module_flat.txt') -Raw
    }

    It 'Writes a manifest and a flat text file named after the module' {
        $Manifest.ModuleRoot | Should -Be 'my.module'
        $FlatText | Should -Not -BeNullOrEmpty
    }

    It 'Lists files and folders with forward-slash relative paths' {
        $Manifest.Items.RelativePath | Should -Contain 'Public/Get-Thing.ps1'
        $Manifest.Items.RelativePath | Should -Contain 'Public/Tests'
        ($Manifest.Items | Where-Object RelativePath -EQ 'Public/Get-Thing.ps1').ParentPath | Should -Be 'Public'
        ($Manifest.Items | Where-Object RelativePath -EQ 'Public').ItemType | Should -Be 'Directory'
    }

    It 'Does not record absolute paths in the manifest' {
        $Manifest.Items[0].PSObject.Properties.Name | Should -Not -Contain 'FullPath'
        (Get-Content -Path (Join-Path -Path $ExportPath -ChildPath 'my.module_manifest.json') -Raw) | Should -Not -Match ([regex]::Escape(($TestDrive -replace '\\', '\\')))
    }

    It 'Records whether each file starts with a UTF-8 byte order mark' {
        $bomFolder = Join-Path -Path $TestDrive -ChildPath 'bom/bom.module'
        $null = New-Item -Path $bomFolder -ItemType Directory -Force
        [System.IO.File]::WriteAllBytes((Join-Path -Path $bomFolder -ChildPath 'with-bom.ps1'), [byte[]](0xEF, 0xBB, 0xBF, 0x23, 0x20, 0xC3, 0xA9))
        [System.IO.File]::WriteAllBytes((Join-Path -Path $bomFolder -ChildPath 'no-bom.ps1'), [byte[]](0x23, 0x20, 0x61))
        $bomExport = Join-Path -Path $TestDrive -ChildPath 'bom-export'
        Export-ModuleToFlatTextAndManifest -ModulePath $bomFolder -OutputPath $bomExport
        $bomManifest = Get-Content -Path (Join-Path -Path $bomExport -ChildPath 'bom.module_manifest.json') -Raw | ConvertFrom-Json
        ($bomManifest.Items | Where-Object RelativePath -EQ 'with-bom.ps1').Utf8Bom | Should -BeTrue
        ($bomManifest.Items | Where-Object RelativePath -EQ 'no-bom.ps1').Utf8Bom | Should -BeFalse
    }

    It 'Wraps each file in start and end markers' {
        $FlatText | Should -Match '###FILE-START:Public/Get-Thing.ps1###'
        $FlatText | Should -Match '###FILE-END:Public/Get-Thing.ps1###'
    }

    It 'Skips binary files with a warning' {
        $Manifest.Items.RelativePath | Should -Not -Contain 'icon.png'
        $exportWarnings | Should -Not -BeNullOrEmpty
    }

    It 'Works with a relative module path and a trailing separator' {
        Push-Location -Path (Join-Path -Path $TestDrive -ChildPath 'src')
        try {
            $relativeExport = Join-Path -Path $TestDrive -ChildPath 'export-relative'
            Export-ModuleToFlatTextAndManifest -ModulePath ('my.module' + [System.IO.Path]::DirectorySeparatorChar) -OutputPath $relativeExport -WarningAction SilentlyContinue
            $relativeManifest = Get-Content -Path (Join-Path -Path $relativeExport -ChildPath 'my.module_manifest.json') -Raw | ConvertFrom-Json
            $relativeManifest.Items.RelativePath | Should -Contain 'Public/Get-Thing.ps1'
        }
        finally {
            Pop-Location
        }
    }

    It 'Throws when the module path does not exist' {
        { Export-ModuleToFlatTextAndManifest -ModulePath (Join-Path -Path $TestDrive -ChildPath 'missing') -OutputPath $ExportPath } | Should -Throw
    }

    Context 'Telemetry' {
        BeforeAll {
            Mock -ModuleName tcs.utils Invoke-TelemetryCollection { }
        }

        It 'Sends Start and End events when the export succeeds' {
            Export-ModuleToFlatTextAndManifest -ModulePath $ModuleSource -OutputPath (Join-Path -Path $TestDrive -ChildPath 'export-telemetry') -WarningAction SilentlyContinue
            Should -Invoke -ModuleName tcs.utils Invoke-TelemetryCollection -Times 1 -Exactly -ParameterFilter { $Stage -eq 'Start' -and $CommandName -eq 'Export-ModuleToFlatTextAndManifest' }
            Should -Invoke -ModuleName tcs.utils Invoke-TelemetryCollection -Times 1 -Exactly -ParameterFilter { $Stage -eq 'End' -and -not $Failed }
        }

        It 'Sends a failed End event and still throws when the module path does not exist' {
            { Export-ModuleToFlatTextAndManifest -ModulePath (Join-Path -Path $TestDrive -ChildPath 'missing') -OutputPath $ExportPath } | Should -Throw '*does not exist*'
            Should -Invoke -ModuleName tcs.utils Invoke-TelemetryCollection -Times 1 -Exactly -ParameterFilter { $Stage -eq 'Start' }
            Should -Invoke -ModuleName tcs.utils Invoke-TelemetryCollection -Times 1 -Exactly -ParameterFilter { $Stage -eq 'End' -and $Failed -eq $true -and $null -ne $Exception }
            Should -Invoke -ModuleName tcs.utils Invoke-TelemetryCollection -Times 0 -Exactly -ParameterFilter { $Stage -eq 'End' -and -not $Failed }
        }
    }
}