tests/DBOpsBuild.class.Tests.ps1

Param (
    [switch]$Batch
)

if ($PSScriptRoot) { $commandName = $MyInvocation.MyCommand.Name.Replace(".Tests.ps1", ""); $here = $PSScriptRoot }
else { $commandName = "_ManualExecution"; $here = (Get-Item . ).FullName }

if (!$Batch) {
    # Is not a part of the global batch => import module
    #Explicitly import the module for testing
    Import-Module "$here\..\dbops.psd1" -Force; Get-DBOModuleFileList -Type internal | ForEach-Object { . $_.FullName }
}
else {
    # Is a part of a batch, output some eye-catching happiness
    Write-Host "Running $commandName tests" -ForegroundColor Cyan
}

Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
. "$here\..\internal\classes\DBOpsHelper.class.ps1"
. "$here\..\internal\classes\DBOps.class.ps1"

$packageName = Join-PSFPath -Normalize "$here\etc\$commandName.zip"
$script1 = Join-PSFPath -Normalize "$here\etc\sqlserver-tests\success\1.sql"
$script2 = Join-PSFPath -Normalize "$here\etc\sqlserver-tests\success\2.sql"
$script3 = Join-PSFPath -Normalize "$here\etc\sqlserver-tests\success\3.sql"

$fileObject1 = Get-Item $script1
$fileObject2 = Get-Item $script2
$fileObject3 = Get-Item $script3

$scriptPath1 = Join-PSFPath -Normalize 'success\1.sql'
$scriptPath2 = Join-PSFPath -Normalize 'success\2.sql'
$scriptPath3 = Join-PSFPath -Normalize 'success\3.sql'

Describe "DBOpsBuild class tests" -Tag $commandName, UnitTests, DBOpsBuild {
    Context "tests DBOpsBuild object creation" {
        It "Should create new DBOpsBuild object" {
            $b = [DBOpsBuild]::new('1.0')
            $b.Build | Should Be '1.0'
            $b.PackagePath | Should Be '1.0'
            ([datetime]$b.CreatedDate).Date | Should Be ([datetime]::Now).Date
        }
        It "Should create new DBOpsBuild object using custom object" {
            $obj = @{
                Build       = '2.0'
                PackagePath = '2.00'
                CreatedDate = (Get-Date).Date
            }
            $b = [DBOpsBuild]::new($obj)
            $b.Build | Should Be $obj.Build
            $b.PackagePath | Should Be $obj.PackagePath
            $b.CreatedDate | Should Be $obj.CreatedDate
        }
    }
    Context "tests DBOpsBuild file adding methods" {
        AfterAll {
            if (Test-Path $packageName) { Remove-Item $packageName }
        }
        BeforeAll {
        }
        BeforeEach {
            $pkg = [DBOpsPackage]::new()
            $pkg.SaveToFile($packageName, $true)
            $build = $pkg.NewBuild('1.0')
        }
        It "Should test AddScript([string]) method" {
            $f = [DBOpsFile]::new($fileObject1, $scriptPath1, $true)
            $build.AddScript($f)
            #test build to contain the script
            '1.sql' | Should BeIn $build.Scripts.Name
            ($build.Scripts | Measure-Object).Count | Should Be 1
        }
        It "Should test AddScript([string],[bool]) method" {
            $f = [DBOpsFile]::new($fileObject1, $scriptPath1, $true)
            $build.AddScript($f, $false)
            #test build to contain the script
            '1.sql' | Should BeIn $build.Scripts.Name
            ($build.Scripts | Measure-Object).Count | Should Be 1
            { $build.AddScript($f, $false) } | Should Throw 'already exists'
            ($build.Scripts | Measure-Object).Count | Should Be 1
            $build.AddScript($f, $true)
            ($build.Scripts | Measure-Object).Count | Should Be 1
            $f2 = [DBOpsFile]::new($fileObject1, $scriptPath2, $true)
            $build.AddScript($f2, $true)
            ($build.Scripts | Measure-Object).Count | Should Be 2
        }
    }
    Context "tests other methods" {
        BeforeEach {
            $pkg = [DBOpsPackage]::new()
            $build = $pkg.NewBuild('1.0')
            $f = [DBOpsFile]::new($fileObject1, $scriptPath1, $true)
            $build.AddScript($f)
            $pkg.SaveToFile($packageName, $true)
        }
        AfterAll {
            if (Test-Path $packageName) { Remove-Item $packageName }
        }
        It "should test ToString method" {
            $build.ToString() | Should Be '[1.0]'
        }
        It "should test GetScript method" {
            $result = $build.GetScript($scriptPath1)
            $result | Should -Not -BeNullOrEmpty
            $result.PackagePath | Should -Be $scriptPath1
            $f2 = [DBOpsFile]::new($fileObject2, $scriptPath2, $true)
            $build.AddScript($f2)
            $result = $build.GetScript(@($scriptPath1, $scriptPath2))
            $result | Should -Not -BeNullOrEmpty
            $result.PackagePath | Should -Be $scriptPath1, $scriptPath2
            $result = $build.GetScript($scriptPath3)
            $result | Should -BeNullOrEmpty
        }
        It "should test RemoveScript method" {
            $f2 = [DBOpsFile]::new($fileObject2, $scriptPath2, $true)
            $build.AddScript($f2)
            $build.RemoveScript($scriptPath1)
            $build.Scripts.PackagePath | Should -Be $scriptPath2
            { $build.RemoveScript($scriptPath1) } | Should -Throw "File $scriptPath1 not found"
            $build.RemoveScript($scriptPath2)
            $build.Scripts | Should -BeNullOrEmpty
            { $build.RemoveScript($scriptPath1) } | Should -Throw "Collection Scripts not found or empty"
        }
        It "should test HashExists method" {
            $build.HashExists($f.Hash) | Should Be $true
            $build.HashExists('foo') | Should Be $false
            $build.HashExists($f.Hash, $scriptPath1) | Should Be $true
            $build.HashExists($f.Hash, 'bar') | Should Be $false
            $build.HashExists('foo', $scriptPath1) | Should Be $false
        }
        It "should test ScriptExists method" {
            $build.ScriptExists($script1) | Should Be $true
            $build.ScriptExists((Join-PSFPath -Normalize "$here\etc\sqlserver-tests\transactional-failure\1.sql")) | Should Be $false
            { $build.ScriptExists("Nonexisting\path") } | Should Throw
        }
        It "should test ScriptModified method" {
            $build.ScriptModified([DBOpsFile]::new($fileObject1, $scriptPath1, $true)) | Should Be $false
            $build.ScriptModified([DBOpsFile]::new($fileObject2, $scriptPath1, $true)) | Should Be $true
            $build.ScriptModified([DBOpsFile]::new($fileObject1, $scriptPath2, $true)) | Should Be $false
        }
        It "should test PackagePathExists method" {
            $s1 = Join-PSFPath -Normalize "success\1.sql"
            $s2 = Join-PSFPath -Normalize "success\2.sql"
            $build.PackagePathExists($s1) | Should Be $true
            $build.PackagePathExists($s2) | Should Be $false
        }
        It "should test GetPackagePath method" {
            $build.GetPackagePath() | Should Be (Join-PSFPath -Normalize 'content\1.0')
        }
        It "should test ExportToJson method" {
            $j = $build.ExportToJson() | ConvertFrom-Json
            $j.Scripts | Should Not BeNullOrEmpty
            $j.Build | Should Be '1.0'
            $j.PackagePath | Should Be '1.0'
            $j.CreatedDate | Should Not BeNullOrEmpty
            $j.psobject.properties.name | Should -BeIn @('Scripts', 'Build', 'PackagePath', 'CreatedDate')
            foreach ($script in $j.Scripts) {
                $script.psobject.properties.name | Should -BeIn @('Hash', 'PackagePath')
            }
        }
    }
    Context "tests Save/Alter methods" {
        AfterAll {
            if (Test-Path $packageName) { Remove-Item $packageName }
            if (Test-Path "$packageName.test.zip") { Remove-Item "$packageName.test.zip" }
        }
        BeforeAll {
            if (Test-Path $packageName) { Remove-Item $packageName }
        }
        It "should test Save method" {
            #Generate new package file
            $pkg = [DBOpsPackage]::new()
            $pkg.SaveToFile($packageName, $true)
            $build = $pkg.NewBuild('1.0')
            $f = [DBOpsFile]::new($fileObject1, $scriptPath1, $true)
            $build.AddScript($f)
            $f = [DBOpsFile]::new($fileObject2, $scriptPath2, $true)
            $build.AddScript($f)
            #Open zip file stream
            $writeMode = [System.IO.FileMode]::Open
            $stream = [FileStream]::new($packageName, $writeMode)
            try {
                #Open zip file
                $zip = [ZipArchive]::new($stream, [ZipArchiveMode]::Update)
                try {
                    #Initiate saving
                    { $build.Save($zip) } | Should Not Throw
                }
                catch {
                    throw $_
                }
                finally {
                    #Close archive
                    $zip.Dispose()
                }
            }
            catch {
                throw $_
            }
            finally {
                #Close archive
                $stream.Dispose()
            }
            $testResults = Get-ArchiveItem $packageName
            foreach ($file in (Get-DBOModuleFileList)) {
                Join-PSFPath -Normalize 'Modules\dbops' $file.Path | Should BeIn $testResults.Path
            }
            'dbops.config.json' | Should BeIn $testResults.Path
            'dbops.package.json' | Should BeIn $testResults.Path
            'Deploy.ps1' | Should BeIn $testResults.Path
            Join-PSFPath -Normalize 'content\1.0\success\1.sql' | Should BeIn $testResults.Path
            Join-PSFPath -Normalize 'content\1.0\success\2.sql' | Should BeIn $testResults.Path
        }
        It "Should load package successfully after saving it" {
            $p = [DBOpsPackage]::new($packageName)
            $p.Builds.Scripts.Name | Should Not Be @('1.sql', '2.sql') #Build.Save method does not write to package file
        }
        It "Should save and reopen the package under a different name" {
            #Generate new package file
            $pkg = [DBOpsPackage]::new()
            $pkg.SaveToFile($packageName, $true)
            $b = $pkg.NewBuild('1.0')
            $f = [DBOpsFile]::new($fileObject1, $scriptPath1, $true)
            $b.AddScript($f)
            $f = [DBOpsFile]::new($fileObject2, $scriptPath2, $true)
            $b.AddScript($f)
            $pkg.SaveToFile("$packageName.test.zip")
            $pkg = [DBOpsPackage]::new("$packageName.test.zip")
            $pkg.GetBuild('1.0').Scripts.Name | Should Be @('1.sql', '2.sql')
        }
        $oldResults = Get-ArchiveItem "$packageName.test.zip"
        #Sleep 1 second to ensure that modification date is changed
        Start-Sleep -Seconds 2
        It "should test Alter method" {
            $pkg = [DBOpsPackage]::new("$packageName.test.zip")
            $build = $pkg.GetBuild('1.0')
            $f = [DBOpsFile]::new($fileObject3, (Join-PSFPath -Normalize 'success\3.sql'), $true)
            $build.AddScript($f)
            { $build.Alter() } | Should Not Throw
            $testResults = Get-ArchiveItem "$packageName.test.zip"
            foreach ($file in (Get-DBOModuleFileList)) {
                Join-PSFPath -Normalize  'Modules\dbops' $file.Path | Should BeIn $testResults.Path
            }
            'dbops.config.json' | Should BeIn $testResults.Path
            'dbops.package.json' | Should BeIn $testResults.Path
            'Deploy.ps1' | Should BeIn $testResults.Path
            Join-PSFPath -Normalize 'content\1.0\success\1.sql' | Should BeIn $testResults.Path
            Join-PSFPath -Normalize 'content\1.0\success\2.sql' | Should BeIn $testResults.Path
        }
        It "Should load package successfully after saving it" {
            $p = [DBOpsPackage]::new("$packageName.test.zip")
            $p.Builds.Scripts.Name | Should Be @('1.sql', '2.sql', '3.sql')
        }
        # Testing file contents to be updated by the Save method
        $testResults = Get-ArchiveItem "$packageName.test.zip"
        $saveTestsErrors = 0
        #should trigger file updates for build files and module files
        foreach ($testResult in ($oldResults | Where-Object { $_.Path -like (Join-PSFPath -Normalize 'content\1.0\success\*') -or $_.Path -like (Join-PSFPath -Normalize 'Modules\dbops\*')  } )) {
            if ($testResult.LastWriteTime -ge ($testResults | Where-Object Path -eq $testResult.Path).LastWriteTime) {
                It "Should have updated Modified date for file $($testResult.Path)" {
                    $testResult.LastWriteTime -lt ($testResults | Where-Object Path -eq $testResult.Path).LastWriteTime | Should Be $true
                }
                $saveTestsErrors++
            }
        }
        if ($saveTestsErrors -eq 0) {
            It "Ran silently $($oldResults.Length) file modification tests" {
                $saveTestsErrors | Should -Be 0
                $oldResults.Length | Should -BeGreaterThan 0
            }
        }
    }
}