Specs/Configuration.Steps.ps1

$PSModuleAutoLoadingPreference = "None"

Remove-Module Configuration -EA 0
Import-Module .\Configuration.psd1

Given 'the configuration module is imported with testing paths:' {
    param($Table)
    Remove-Module Configuration -EA 0
    Import-Module .\Configuration.psd1 -Args @($null, $Table.Enterprise, $Table.User, $Table.Machine) -Scope Global
}

Given 'the configuration module is imported with a URL converter' {
    param($Table)
    Remove-Module Configuration -EA 0
    Import-Module .\Configuration.psd1 -Args @{
                [Uri] = { "Uri '$_' " }
                "Uri" = {
                    param([string]$Value)
                    [Uri]$Value
                }
            } -Scope Global
}

Given "a module with(?:\s+\w+ name '(?<name>.+?)'|\s+\w+ the company '(?<company>.+?)'|\s+\w+ the author '(?<author>.+?)')+" {
    param($name, $Company = "", $Author = "")

    $Script:ModulePath = "TestDrive:\Modules\$name"
    Remove-Module $name -ErrorAction SilentlyContinue
    Remove-Item $ModulePath -Recurse -ErrorAction SilentlyContinue
    $null = mkdir $ModulePath -Force
    $Env:PSModulePath = $Env:PSModulePath + ";TestDrive:\Modules" -replace "(;TestDrive:\\Modules)+?$", ";TestDrive:\Modules"

    Set-Content $ModulePath\${Name}.psm1 "
    function GetStoragePath { Get-StoragePath @Args }
    function ImportConfiguration { Import-Configuration }
    function ImportConfigVersion { Import-Configuration -Version 2.0 }
    filter ExportConfiguration { `$_ | Export-Configuration }
    filter ExportConfigVersion { `$_ | Export-Configuration -Version 2.0 }
    "


    New-ModuleManifest $ModulePath\${Name}.psd1 -RootModule .\${Name}.psm1 -Description "A Super Test Module" -Company $Company -Author $Author

    # New-ModuleManifest sets things even when we don't want it to:
    if(!$Author) {
        Set-Content $ModulePath\${Name}.psd1 ((Get-Content $ModulePath\${Name}.psd1) -Replace "^(Author.*)$", '#$1')
    }
    if(!$Company) {
        Set-Content $ModulePath\${Name}.psd1 ((Get-Content $ModulePath\${Name}.psd1) -Replace "^(Company.*)$", '#$1')
    }

    Import-Module $ModulePath\${Name}.psd1
}

When "the module's (\w+) path should (\w+) (.+)$" {
    param($Scope, $Comparator, $Path)

    [string[]]$Path = $Path -split "\s*and\s*" | %{ $_.Trim("['`"]") }

    $script:LocalStoragePath = GetStoragePath -Scope $Scope
    foreach($PathAssertion in $Path) {
        $script:LocalStoragePath | Should $Comparator $PathAssertion
    }
}

Given "a script with the name '(.+)' that calls Get-StoragePath with no parameters" {
    param($name)
    Set-Content "TestDrive:\${name}.ps1" "Get-StoragePath"
    $Script:ScriptName = $Name
}
Given "a script with the name '(?<File>.+)' that calls Get-StoragePath (?:-Name (?<Name>\w*) ?|-Author (?<Author>\w*) ?){2}" {
    param($File, $Name, $Author)
    Set-Content "TestDrive:\${File}.ps1" "Get-StoragePath -Name $Name -Author $Author"
    $Script:ScriptName = $File
}

Then "the script should throw an exception$" {
    { $script:LocalStoragePath = iex "TestDrive:\${ScriptName}.ps1" } | Should throw
}


Then "the script's (\w+) path should (\w+) (.+)$" {
    param($Scope, $Comparator, $Path)

    [string[]]$Path = $Path -split "\s*and\s*" | %{ $_.Trim("['`"]") }

    $script:LocalStoragePath = iex "TestDrive:\${Script:ScriptName}.ps1"
    foreach($PathAssertion in $Path) {
        $script:LocalStoragePath | Should $Comparator $PathAssertion
    }
}

When "the module's storage path should end with a version number if one is passed in" {
    GetStoragePath -Version "2.0" | Should Match "\\2.0$"
    GetStoragePath -Version "4.0" | Should Match "\\4.0$"
}


When "a settings hashtable" {
    param($hashtable)
    $script:Settings = iex "[ordered]$hashtable"
}

When "we update the settings with" {
    param($hashtable)
    $Update = if($hashtable) {
        iex $hashtable
    } else {
        $null
    }

    $script:Settings = $script:Settings | Update-Object $Update
}

When "a settings file named (\S+)(?:(?: in the (?<Scope>\S+) folder)|(?: for version (?<Version>[0-9.]+)))*" {
    param($fileName, $hashtable, $Scope = $null, $Version = $null)

    if($Scope -and $Version) {
        $folder = GetStoragePath -Scope $Scope -Version $Version
    } elseif($Scope) {
        $folder = GetStoragePath -Scope $Scope
    } elseif($Version) {
        $folder = GetStoragePath -Version $Version
    } elseif(Test-Path "${Script:ModulePath}") {
        $folder = $Script:ModulePath
    } else {
        $folder = "TestDrive:\"
    }
    $Script:SettingsFile = Join-Path $folder $fileName

    $Parent = Split-Path $Script:SettingsFile
    if(!(Test-Path $Parent)) {
        $null = mkdir $Parent -Force -EA 0
    }
    Set-Content $Script:SettingsFile -Value $hashtable
}

Then "the settings object MyPath should match the file's path" {
    $script:Settings.MyPath | Should Be ${Script:SettingsFile}
}

When "a settings hashtable with an? (.+) in it" {
    param($type)
    $script:Settings = @{
        UserName = $Env:UserName
    }

    switch($type) {
        "NULL" {
            $Settings.TestCase = $Null
        }
        "Enum" {
            $Settings.TestCase = [Security.PolicyLevelType]::Enterprise
        }
        "String" {
            $Settings.TestCase = "Test"
        }
        "Number" {
            $Settings.OneTestCase = 42
            $Settings.TwoTestCase = 42.9
        }
        "Array"  {
            $Settings.TestCase = "One", "Two", "Three"
        }
        "Boolean"  {
            $Settings.OneTestCase = $True
            $Settings.TwoTestCase = $False
        }
        "DateTime" {
            $Settings.TestCase = Get-Date
        }
        "DateTimeOffset" {
            $Settings.TestCase = [DateTimeOffset](Get-Date)
        }
        "GUID" {
            $Settings.TestCase = [GUID]::NewGuid()
        }
        "PSObject" {
            $Settings.TestCase = New-Object PSObject -Property @{ Name = $Env:UserName }
        }
        "PSCredential" {
            $Settings.TestCase = New-Object PSCredential @("UserName", (ConvertTo-SecureString -AsPlainText -Force -String "Password"))
        }
        "SecureString" {
            $Settings.TestCase = ConvertTo-SecureString -AsPlainText -Force -String "Password"
        }
        "Uri" {
            $Settings.TestCase = [Uri]"http://HuddledMasses.org"
        }
        "Hashtable" {
            $Settings.TestCase = @{ Key = "Value"; ANother = "Value" }
        }
        default {
            throw "missing test type"
        }
    }
}

When "we add a converter for (.*) types" {
    param($Type)
    switch ($Type) {
        "Uri" {
            Add-MetadataConverter @{
                [Uri] = { "Uri '$_' " }
                "Uri" = {
                    param([string]$Value)
                    [Uri]$Value
                }
            }
        }
        default {
            throw "missing converter type"
        }
    }
}

When "we convert the settings to metadata" {
    $script:SettingsMetadata = ConvertTo-Metadata $script:Settings

    # Write-Debug $script:SettingsMetadata
    $Wide = $Host.UI.RawUI.WindowSize.Width
    Write-Verbose $script:SettingsMetadata
}

When "we export to a settings file named (.*)" {
    param($fileName)
    if(!$Script:ModulePath -or !(Test-Path $Script:ModulePath)) {
        $Script:ModulePath = "TestDrive:\"
    }
    $Script:SettingsFile = Join-Path $Script:ModulePath $fileName
    $File = $script:Settings | Export-Metadata ${Script:SettingsFile} -Passthru
    $File.FullName | Should Be (Convert-Path $SettingsFile)
}


When "we convert the metadata to an object" {
    $script:Settings = ConvertFrom-Metadata $script:SettingsMetadata

    Write-Verbose (($script:Settings | Out-String -Stream | % TrimEnd) -join "`n")
}


When "we import the file to an object" {
    $script:Settings = Import-Metadata ${Script:SettingsFile}

    Write-Verbose (($script:Settings | Out-String -Stream | % TrimEnd) -join "`n")
}


When "we import the file with ordered" {
    $script:Settings = Import-Metadata ${Script:SettingsFile} -Ordered

    Write-Verbose (($script:Settings | Out-String -Stream | % TrimEnd) -join "`n")
}

When "we import the folder path" {
    $script:Settings = Import-Metadata (Split-Path ${Script:SettingsFile})

    Write-Verbose (($script:Settings | Out-String -Stream | % TrimEnd) -join "`n")
}

When "trying to import the file to an object should throw(.*)" {
    param([string]$Message)
    { $script:Settings = Import-Metadata ${Script:SettingsFile} } | Should Throw $Message.trim()
}

When "the string version should (\w+)\s*(.*)?" {
    param($operator, $data)
    # I have to normalize line endings:
    $meta = ($script:SettingsMetadata -replace "\r?\n","`n")
    $data = $data.trim('"''')  -replace "\r?\n","`n"
    # And then actually test it
    $meta | Should $operator $data
}

When "the settings file should (\w+)\s*(.*)?" {
    param($operator, $data)
                    # I have to normalize line endings:
    $data = [regex]::escape(($data -replace "\r?\n","`n"))
    if($operator -eq "Contain"){ $operator = "ContainMultiline"}
    Get-Item ${Script:SettingsFile} | Should $operator $data
}
# This step will create verifiable/counting loggable mocks for Write-Warning, Write-Error, Write-Verbose
When "we expect an? (?<type>warning|error|verbose)" {
    param($type)
    Mock -Module Metadata Write-$type { $true } -Verifiable
    if($Type -eq "Error") {
        Mock -Module Metadata WriteError { Write-Error "Error" -TargetObject $Args } -Verifiable
        Mock -Module Metadata ThrowError { Write-Error "Error" -TargetObject $Args } -Verifiable
    }
}

# this step lets us verify the number of calls to those three mocks
When "the (?<type>warning|error|verbose) is logged(?: (?<exactly>exactly) (\d+) times)?" {
    param($count, $exactly, $type)
    $param = @{}
    if($count) {
        $param.Exactly = $Exactly -eq "Exactly"
        $param.Times = $count
    }
    Assert-MockCalled -Module Metadata -Command Write-$type @param
}

When "we add a converter that's not a scriptblock" {
    Add-MetadataConverter @{
        "Uri" = "
            param([string]$Value)
            [Uri]$Value
        "

    }
}

When "we add a converter with a number as a key" {
    Add-MetadataConverter @{
        42 = {
            param([string]$Value)
            $Value
        }
    }
}

# Then the error is logged exactly 2 times

Then "the settings object should be of type (.*)" {
    param([Type]$Type)
    $script:Settings | Should BeOfType $Type
}


Then "the settings object should have an? (.*) of type (.*)" {
    param([String]$Parameter, [Type]$Type)
    $script:Settings.$Parameter | Should BeOfType $Type
}

Then "the settings object's (.*) should (be of type|be) (.*)" {
    param([String]$Parameter, [String]$operator, $Expected)
    $Value = $script:Settings

    # Write-Debug ($Settings | Out-String)

    foreach($property in $Parameter.Split(".")) {
        $value = $value.$property
    }

    $operator = $operator -replace " "

    if($Operator -eq "be" -and $Expected -eq "null") {
        $value | Should BeNullOrEmpty
    } else {
        $value | Should $operator $Expected
    }
}

Then "Key (\d) is (\w)" {
    param([int]$index, [string]$name)
    $script:Settings.Keys[$index] | Should Be $Name   
}

Given "a mock PowerShell version (.*)" {
    param($version)
    $script:PSVersion = [Version]$version
    $script:PSDefaultParameterValues."Test-PSVersion:Version" = $script:PSVersion
}

When "we fake version 2.0 in the Metadata module" {
    &(Get-Module Configuration) {
        &(Get-Module Metadata) {
            $script:PSDefaultParameterValues."Test-PSVersion:Version" = [Version]"2.0"
        }
    }
}

When "we're using PowerShell 4 or higher in the Metadata module" {
    &(Get-Module Configuration) {
        &(Get-Module Metadata) {
            $null = $script:PSDefaultParameterValues.Remove("Test-PSVersion:Version")
            $PSVersionTable.PSVersion -ge ([Version]"4.0") | Should Be $True
        }
    }
}

Given "the actual PowerShell version" {
    $script:PSVersion = $PSVersionTable.PSVersion
    $null = $script:PSDefaultParameterValues.Remove("Test-PSVersion:Version")
}

Then "the Version -(..) (.*)" {
    param($comparator, $version)

    if($version -eq "the version") {
        [Version]$version = $script:PSVersion
    } else {
        [Version]$version = $version
    }

    $test = @{ $comparator = $version }
    Test-PSVersion @test | Should Be $True
}

When "I call Import-Configuration" {
    $script:Settings = ImportConfiguration

    Write-Verbose (($script:Settings | Out-String -Stream | % TrimEnd) -join "`n")
}

When "I call Import-Configuration with a Version" {
    $script:Settings = ImportConfigVersion

    Write-Verbose (($script:Settings | Out-String -Stream | % TrimEnd) -join "`n")
}

When "I call Export-Configuration with" {
    param($configuration)
    iex "$configuration" | ExportConfiguration
}

When "I call Export-Configuration with a Version" {
    param($configuration)
    iex "$configuration" | ExportConfigVersion
}

Then "a settings file named (\S+) should exist(?:(?: in the (?<Scope>\S+) folder)|(?: for version (?<Version>[0-9.]+)))*" {
    param($fileName, $hashtable, $Scope = $null, $Version = $null)

    if($Scope -and $Version) {
        $folder = GetStoragePath -Scope $Scope -Version $Version
    } elseif($Scope) {
        $folder = GetStoragePath -Scope $Scope
    } elseif($Version) {
        $folder = GetStoragePath -Version $Version
    } elseif(Test-Path "${Script:ModulePath}") {
        $folder = $Script:ModulePath
    } else {
        $folder = "TestDrive:\"
    }
    $Script:SettingsFile = Join-Path $folder $fileName
    $Script:SettingsFile | Should Exist
}