Tests/QA/Localization.common.v5.Tests.ps1

<#
    .NOTES
        To run manually:
 
        $dscResourceModuleName = 'FileSystemDsc'
        $pathToHQRMTests = Join-Path -Path (Get-Module DscResource.Test).ModuleBase -ChildPath 'Tests\QA'
 
        $container = New-PesterContainer -Path "$pathToHQRMTests/Localization.common.*.Tests.ps1" -Data @{
            ModuleBase = "./output/$dscResourceModuleName/*"
            # SourcePath = './source'
            # ExcludeModuleFile = @('Modules/DscResource.Common')
            # ExcludeSourceFile = @('Examples')
        }
 
        Invoke-Pester -Container $container -Output Detailed
#>

param
(
    [Parameter(Mandatory = $true)]
    [System.String]
    $ModuleBase,

    [Parameter()]
    [System.String]
    $SourcePath,

    [Parameter()]
    [System.String[]]
    $ExcludeModuleFile,

    [Parameter()]
    [System.String[]]
    $ExcludeSourceFile,

    [Parameter(ValueFromRemainingArguments = $true)]
    $Args
)

# This test _must_ be outside the BeforeDiscovery-block since Pester 4 does not recognizes it.
$isPester5 = (Get-Module -Name Pester).Version -ge '5.1.0'

# Only run if Pester 5.1.
if (-not $isPester5)
{
    Write-Verbose -Message 'Repository is using old Pester version, new HQRM tests for Pester 5 are skipped.' -Verbose
    return
}

BeforeDiscovery {
    # Re-imports the private (and public) functions.
    Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../DscResource.Test.psm1') -Force

    $moduleFiles = @(Get-ChildItem -Path $ModuleBase -Filter '*.psm1' -Recurse | WhereModuleFileNotExcluded -ExcludeModuleFile $ExcludeModuleFile)

    if ($SourcePath)
    {
        $moduleFiles += @(Get-ChildItem -Path $SourcePath -Filter '*.psm1' -Recurse | WhereSourceFileNotExcluded -ExcludeSourceFile $ExcludeSourceFile)
    }

    <#
        Exclude empty PSM1. Only expect localization for Module files with some
        functions defined
    #>

    $moduleFiles = $moduleFiles | Where-Object -FilterScript {
        <#
            Ignore parse errors in the script files. Parse error will be caught
            in the tests in ModuleScriptFiles.common.
        #>

        $currentPath = $_.FullName

        try
        {
            Get-FunctionDefinitionAst -FullName $currentPath

            $valid = $true
        }
        catch
        {
            # Outputting the error just in case there is another error than parse error.
            Write-Warning -Message ('File ''{0}'' is skipped because it could not be parsed. Error message: {1}' -f $currentPath, $_.Exception.Message)

            $valid = $false
        }

        $_.Length -gt 0 -and $valid
    }

    $fileToTest = @()

    foreach ($file in $moduleFiles)
    {
        if ($VerbosePreference -ne 'SilentlyContinue')
        {
            Write-Verbose -Message "$($file | ConvertTo-Json)"
        }

        $testProperties = @{
            File                   = $file
            ParentFolderName       = Split-Path -Path $file.Directory.FullName -Leaf
            LocalizationFolderPath = (Join-Path -Path $file.Directory.FullName -ChildPath 'en-US')
            LocalizationFile       = (Join-Path -Path $file.Directory.FullName -ChildPath (Join-Path -Path 'en-US' -ChildPath "$($file.BaseName).strings.psd1"))
        }

        $localizedKeyToTest = @()
        $usedLocalizedKeyToTest = @()

        <#
            Build test cases for all localized strings that are in the localized
            string file and all that are used in the module file.
 
            Skips a file that do not exist yet (it are caught in a test)
        #>

        if (Test-Path -Path $testProperties.LocalizationFile)
        {
            Import-LocalizedData `
                -BindingVariable 'englishLocalizedStrings' `
                -FileName ('{0}.strings.psd1' -f $testProperties.File.BaseName) `
                -BaseDirectory $testProperties.LocalizationFolderPath `
                -UICulture 'en-US'

            foreach ($localizedKey in $englishLocalizedStrings.Keys)
            {
                $localizedKeyToTest += @{
                    LocalizedKey = $localizedKey
                }
            }

            $parseErrors = $null

            $definitionAst = [System.Management.Automation.Language.Parser]::ParseFile($testProperties.File.FullName, [ref] $null, [ref] $parseErrors)

            if ($parseErrors)
            {
                throw $parseErrors
            }

            $astFilter = {
                $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst] `
                    -and $args[0].Parent -is [System.Management.Automation.Language.MemberExpressionAst] `
                    -and $args[0].Parent.Expression -is [System.Management.Automation.Language.VariableExpressionAst] `
                    -and $args[0].Parent.Expression.VariablePath.UserPath -eq 'script:localizedData'
            }

            $localizationStringConstantsAst = $definitionAst.FindAll($astFilter, $true)

            if ($localizationStringConstantsAst)
            {
                $usedLocalizationKeys = $localizationStringConstantsAst.Value | Sort-Object -Unique

                foreach ($localizedKey in $usedLocalizationKeys)
                {
                    $usedLocalizedKeyToTest += @{
                        LocalizedKey = $localizedKey
                    }
                }
            }
        }

        $testProperties.EnUsLocalizedKeys = $localizedKeyToTest
        $testProperties.UsedLocalizedKeys = $usedLocalizedKeyToTest

        $otherLanguageToTest = @()

        # Get all localization folders except the en-US (regardless of casing).
        $localizationFolders = Get-ChildItem -Path $file.Directory.FullName -Directory -Filter '*-*' |
            Where-Object -FilterScript {
                $_.Name -ne 'en-US'
            }

        foreach ($localizationFolder in $localizationFolders)
        {
            $cultureToTest = @{
                LocalizationFolderName = Split-Path -Path $localizationFolder.FullName -Leaf
                LocalizationFolderPath = $localizationFolder.FullName
            }

            $localizedKeyToTest = @()

            <#
                Build test cases for all localized strings that are in the culture's
                localized string file.
 
                Skips a file that do not exist yet (it are caught in a test)
            #>

            if (Test-Path -Path $cultureToTest.LocalizationFolderPath)
            {
                Import-LocalizedData `
                    -BindingVariable 'cultureLocalizedStrings' `
                    -FileName "$($testProperties.File.BaseName).strings.psd1" `
                    -BaseDirectory $cultureToTest.LocalizationFolderPath `
                    -UICulture $otherLanguageToTest.LocalizationFolderName

                foreach ($localizedKey in $cultureLocalizedStrings.Keys)
                {
                    $localizedKeyToTest += @{
                        CultureLocalizedKey = $localizedKey
                    }
                }
            }

            $cultureToTest.CultureLocalizedKeys = $localizedKeyToTest

            $otherLanguageToTest += $cultureToTest
        }

        $testProperties.OtherLanguages = $otherLanguageToTest

        $fileToTest += $testProperties
    }
}

BeforeAll {
    # Re-imports the private (and public) functions.
    Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../DscResource.Test.psm1') -Force
}

AfterAll {
    # Re-import just the public functions.
    Import-Module -Name 'DscResource.Test' -Force
}

Describe 'Common Tests - Validate Localization' -Tag 'Common Tests - Validate Localization' {
    Context 'When a resource or module ''<ParentFolderName>'' exist' -ForEach $fileToTest {
        It 'Should have en-US localization folder' {
            Test-Path -Path $LocalizationFolderPath | Should -BeTrue -Because "the en-US folder $LocalizationFolderPath must exist"
        }

        It 'Should have en-US localization folder with the correct casing' {
            <#
                This will return both 'en-us' and 'en-US' folders so we can
                evaluate casing.
            #>

            $localizationFolderOnDisk = Get-Item -Path $LocalizationFolderPath -ErrorAction 'SilentlyContinue'
            $localizationFolderOnDisk.Name | Should -MatchExactly 'en-US' -Because 'the en-US folder must have the correct casing'
        }

        It 'Should have en-US localization string resource file' {
            Test-Path -Path $LocalizationFile | Should -BeTrue -Because "the string resource file $LocalizationFile must exist in the localization folder en-US"
        }

        Context 'When the en-US localized resource file have localized strings' {
            <#
                This ForEach is using the key EnUsLocalizedKeys from inside the $fileToTest
                that is set on the Context-block's ForEach above.
            #>

            It 'Should use the localized string key ''<LocalizedKey>'' in the code' -ForEach $EnUsLocalizedKeys {
                $UsedLocalizedKeys.LocalizedKey | Should -Contain $LocalizedKey -Because 'the key exists in the localized string resource file so it should also exist in the resource/module script file'
            }

            <#
                This ForEach is using the key UsedLocalizedKeys from inside the $fileToTest
                that is set on the Context-block's ForEach above.
            #>

            It 'Should not be missing the localized string key ''<LocalizedKey>'' in the localization resource file' -ForEach $UsedLocalizedKeys {
                $EnUsLocalizedKeys.LocalizedKey | Should -Contain $LocalizedKey -Because 'the key is used in the resource/module script file so it should also exist in the localized string resource files'
            }
        }

        Context 'When a resource or module is localized in the language <LocalizationFolderName>' -ForEach $OtherLanguages {
            It 'Should have a localization string file in the localization folder' {
                $localizationResourceFilePath = Join-Path -Path $LocalizationFolderPath -ChildPath "$($File.BaseName).strings.psd1"

                Test-Path -Path $localizationResourceFilePath | Should -BeTrue -Because ('there must exist a string resource file ''{0}.strings.psd1'' in the localization folder ''{1}''' -f $File.BaseName, $LocalizationFolderPath)
            }

            It 'Should be an accurate localization folder with the correct casing' {
                $localizationFolderOnDisk = Get-Item -Path $LocalizationFolderPath -ErrorAction 'SilentlyContinue'
                $localizationFolderOnDisk.Name -cin ([System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures)).Name | Should -BeTrue
            }

            Context 'When the <LocalizationFolderName> localized resource file have localized strings' {
                <#
                    This ForEach is using the key CultureLocalizedKeys from inside the $fileToTest
                    that is set on the Context-block's ForEach above.
                #>

                It 'Should have the string key <CultureLocalizedKey> in the en-US localization resource file' -ForEach $CultureLocalizedKeys {
                    $EnUsLocalizedKeys.LocalizedKey | Should -Contain $CultureLocalizedKey -Because ('the key exists in the {0} localization resource file it must also also exist in the en-US localization resource file' -f $LocalizationFolderName)
                }

                <#
                    This ForEach is using the key EnUsLocalizedKeys from inside the $fileToTest
                    that is set on the Context-block's ForEach above.
                #>

                It 'Should not be missing the localization string key <LocalizedKey>'-ForEach $EnUsLocalizedKeys {
                    $CultureLocalizedKeys.CultureLocalizedKey | Should -Contain $LocalizedKey -Because ('the key exists in the en-US localization resource file so it should also exist in the {0} localization resource file (if you cannot translate the english string in the localized file, then please just add the en-US localization string key together with the en-US text string)' -f $LocalizationFolderName)
                }
            }
        }
    }
}