Tests/QA/Localization.builtModule.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.builtModule.*.Tests.ps1" -Data @{
            ModuleBase = "./output/$dscResourceModuleName/*"
            ProjectPath = '.'
        }

        Invoke-Pester -Container $container -Output Detailed
#>

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

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

    [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

    # trim * from modulebase for -Depth to work
    $localModuleBase = $ModuleBase.Replace('*', '')

    $moduleFiles = @(Get-ChildItem -Path $localModuleBase -Filter '*.psm1' -Depth 1)

    if ($ProjectPath)
    {
        # Expand the project folder if it is a relative path.
        $resolvedProjectPath = (Resolve-Path -Path $ProjectPath).Path
    }
    else
    {
        $resolvedProjectPath = $ModuleBase
    }

    <#
        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
    }

    $scriptLocalizationToTest = @()
    $thisLocalizationToTest = @()

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

        # Use the project folder to extrapolate relative path.
        $descriptiveName = Get-RelativePathFromModuleRoot -FilePath $file.FullName -ModuleRootFilePath $resolvedProjectPath

        $scriptTestProperties = @{
            File                   = $file
            DescriptiveName        = $descriptiveName
            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 $scriptTestProperties.LocalizationFile)
        {
            Import-LocalizedData `
                -BindingVariable 'englishLocalizedStrings' `
                -FileName ('{0}.strings.psd1' -f $scriptTestProperties.File.BaseName) `
                -BaseDirectory $scriptTestProperties.LocalizationFolderPath `
                -UICulture 'en-US'

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

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

            # Look for script:localizedData
            $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
                    }
                }
            }
        }

        $scriptTestProperties.EnUsLocalizedKeys = $localizedKeyToTest
        $scriptTestProperties.UsedLocalizedKeys = $usedLocalizedKeyToTest

        $otherLanguageToTest = @()

        # Get all localization folders except the en-US (regardless of casing).
        $localizationFolders = Get-ChildItem -Path $scriptTestProperties.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 "$($scriptTestProperties.File.BaseName).strings.psd1" `
                    -BaseDirectory $cultureToTest.LocalizationFolderPath `
                    -UICulture $otherLanguageToTest.LocalizationFolderName

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

            $cultureToTest.CultureLocalizedKeys = $localizedKeyToTest

            $otherLanguageToTest += $cultureToTest
        }

        $scriptTestProperties.OtherLanguages = $otherLanguageToTest

        $scriptLocalizationToTest += $scriptTestProperties

        # Now check the class localization for this file
        $classDefinitionAst = Get-ClassDefinitionAst -FullName $file.FullName

        foreach ($class in $classDefinitionAst)
        {
            if ($class.Attributes.TypeName.Name -ieq 'DscResource')
            {
                $classTestProperties = @{
                    ClassName              = $class.Name
                    LocalizationFolderPath = $scriptTestProperties.LocalizationFolderPath
                    LocalizationFile       = (Join-Path -Path $file.Directory.FullName -ChildPath (Join-Path -Path 'en-US' -ChildPath "$($class.Name).strings.psd1"))
                }
            }
            else
            {
                # if this is not a DscResource then skip to next item
                continue
            }

            $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 $classTestProperties.LocalizationFile)
            {
                Import-LocalizedData `
                    -BindingVariable 'englishLocalizedStrings' `
                    -FileName ('{0}.strings.psd1' -f $class.Name) `
                    -BaseDirectory $classTestProperties.LocalizationFolderPath `
                    -UICulture 'en-US'

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

                # Look for $this.localizedData
                $astFilter = {
                    $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
                    $args[0].Parent -is [System.Management.Automation.Language.MemberExpressionAst] -and
                    $args[0].Parent.Expression.Member.Value -eq 'localizedData'
                }

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

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

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

            $classTestProperties.EnUsLocalizedKeys = $localizedKeyToTest
            $classTestProperties.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
                    ClassName              = $class.Name
                }

                $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)
                #>

                $localizationFile = (Join-Path -Path $cultureToTest.LocalizationFolderPath -ChildPath "$($class.Name).strings.psd1")
                $cultureToTest.LocalizationFile = $localizationFile

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

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

                $cultureToTest.CultureLocalizedKeys = $localizedKeyToTest

                $otherLanguageToTest += $cultureToTest
            }

            $classTestProperties.OtherLanguages = $otherLanguageToTest

            $thisLocalizationToTest += $classTestProperties
        }
    }
}

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 'BuiltModule Tests - Validate Localization' -Tag 'BuiltModule Tests - Validate Localization' {
    Context 'When the module ''<DescriptiveName>'' exists' -ForEach $scriptLocalizationToTest {
        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)
                }
            }
        }
    }

    Context 'When the class ''<ClassName>'' exists' -ForEach $thisLocalizationToTest {
        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' {
                Test-Path -Path $LocalizationFile | Should -BeTrue -Because ('there must exist a string resource file ''{0}.strings.psd1'' in the localization folder ''{1}''' -f $ClassName, $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)
                }
            }
        }
    }
}