Tests/Authentication.Tests.ps1

using namespace System.Collections.Generic

BeforeAll {

    $TMModuleDirectory = (Get-ChildItem $PSScriptRoot | Select-Object -First 1).Directory.Parent

    Import-Module "$TMModuleDirectory\TransitionManager.psm1" -Force
    . $TMModuleDirectory\Classes\TransitionManager.Classes.ps1

    $ValidSessions = [List[TMSession]]::new()
    $Global:TMSessions = @{}

    $SessionsWithVersions = @{
        tmad61 = '6.1.2.3'
        tmad62 = '6.2.3.4'
        other61 = '6.1.6.9'
        some60 = '6.0.0.1'
        ya61 = '6.1.6.6'
    }

    foreach ($ServerName in $SessionsWithVersions.Keys) {
        $Session = [TMSession]::new($ServerName, "$ServerName.transitionmanager.net", $true)
        $Session.TMVersion = $SessionsWithVersions.$ServerName
        $ValidSessions.Add($Session)
    }
    $ValidSessions.Item(0).Name = 'Default'

    foreach ($Session in $ValidSessions) {
        $Global:TMSessions.Add($Session.Name, $Session)
    }

    $NonExistingName = 'this-is-a-name-that-does-not-exist'
    $SingleVersion = '6.1.2.3'
    $MultipleVersions = '6.1*'
    $MajorVersion = '6*'
    $NonExistingVersion = '4.0.6.8'
    $SingleServerMatch = $ValidSessions.Item(0).TMServer
    $Multiple61ServerMatch = '*61'
    $MultipleTMADServerMatch = 'tmad*'
}

AfterAll {
    foreach ($Session in $ValidSessions) {
        $Session.Dispose()
    }
}

Describe 'Get-TMSession byObject' {
    It 'Returns the provided valid TMSession' {
        foreach ($ValidSession in $ValidSessions) {
            $Session = Get-TMSession -TMSession $ValidSession
            $Session.GetType().Name | Should -EQ 'TMSession'
        }
    }

    It 'Throws on invalid TMSession' {
        $InvalidSession = [TMSession]::new('my session name', 'my server', $true <# AllowInsecureSSL #>)
        try {
            { Get-TMSession -TMSession $InvalidSession } | Should -Throw
        } finally {
            $InvalidSession.Dispose()
        }
    }
}

Describe 'Get-TMSession by Name' {
    It 'Returns the valid, specified session' {
        foreach ($ValidSession in $ValidSessions) {
            $Session = Get-TMSession -Name $ValidSession.Name
            $Session.GetType().Name | Should -EQ 'TMSession'
            $Session.Count | Should -EQ 1
        }
    }

    It 'Returns all sessiones specified by name' {
        $Sessions = Get-TMSession -Name $ValidSessions.Name
        $Sessions.Count | Should -EQ $ValidSessions.Count
        foreach ($Session in $Sessions) {
            $Session.GetType().Name | Should -EQ 'TMSession'
        }
    }

    It 'Throws if the one specified session is not found' {
        { Get-TMSession -Name $NonExistingName } | Should -Throw
    }
}

Describe 'Get-TMSession by Version' {
    It 'Returns one valid session speicified by Version' {
        $Session = Get-TMSession -Version $SingleVersion
        $Session.GetType().Name | Should -EQ 'TMSession'
        $Session.Count | Should -EQ 1
    }

    It 'Returns all sessions matching major and minor' {
        $Sessions = Get-TMSession -Version $MultipleVersions
        $Sessions.GetType().Name | Should -EQ 'TMSession[]'
    }

    It 'Returns all sessions matching major' {
        $Sessions = Get-TMSession -Version $MajorVersion
        $Sessions.Count | Should -EQ $ValidSessions.Count
    }

    It 'Throws if the one specified session is not found' {
        { Get-TMSession -Version $NonExistingVersion } | Should -Throw
    }
}

Describe 'Get-TMSession by Server' {
    It 'Returns a single session by matching server' {
        $Session = Get-TMSession -Server $SingleServerMatch
        $Session.GetType().Name | Should -EQ 'TMSession'
        $Session.Count | Should -EQ $ValidSessions.Where({$_.TMServer -eq $SingleServerMatch}).Count
    }

    It 'Returns all sessions matching the server name ending in 61' {
        $Sessions = Get-TMSession -Server $Multiple61ServerMatch
        $Sessions.GetType().Name | Should -EQ 'TMSession[]'
        $Sessions.Count | Should -EQ $ValidSessions.Where({$_.TMServer -like "$Multiple61ServerMatch*"}).Count
    }

    It 'Returns all sessions matching the server name starting with "tmad"' {
        $Sessions = Get-TMSession -Server $MultipleTMADServerMatch
        $Sessions.GetType().Name | Should -EQ 'TMSession[]'
        $Sessions.Count | Should -EQ $ValidSessions.Where({$_.TMServer -like "$MultipleTMADServerMatch*"}).Count
    }

    It 'Throws if the specified server is invalid' {
        { Get-TMSession -Server $NonExistingName } | Should -Throw
    }
}

Describe 'TMSession token refresh resource disposal' {
    It 'implements IDisposable and removes its subscriber and event job' {
        $SessionName = 'dispose-' + [Guid]::NewGuid().ToString()
        $EventName = $SessionName + '_TMSession.TokenRefreshTimer.Elapsed_*'
        $Session = [TMSession]::new($SessionName)
        $Timer = $Session.Authentication.OAuth.tokenRefreshTimer

        try {
            $Session -is [System.IDisposable] | Should -BeTrue
            @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 1
            $EventJob = @(Get-Job -Name $EventName -ErrorAction Ignore)
            $EventJob.Count | Should -Be 1
            [Object]::ReferenceEquals($Session.tokenRefreshEventJob, $EventJob[0]) | Should -BeTrue
            $Timer.Enabled | Should -BeTrue

            $Session.Dispose()

            @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 0
            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 0
            { $Timer.Start() } | Should -Throw
            $Session.Authentication.OAuth.tokenRefreshTimer | Should -BeNullOrEmpty
            $Session.tokenRefreshEventJob | Should -BeNullOrEmpty
            $Session.tokenRefreshEventSourceIdentifier | Should -BeNullOrEmpty
        } finally {
            $Session.Dispose()
        }
    }

    It 'can be disposed repeatedly' {
        $SessionName = 'dispose-twice-' + [Guid]::NewGuid().ToString()
        $EventName = $SessionName + '_TMSession.TokenRefreshTimer.Elapsed_*'
        $Session = [TMSession]::new($SessionName)

        { $Session.Dispose(); $Session.Dispose() } | Should -Not -Throw
        @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 0
        @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 0
    }

    It 'does not remove resources owned by another same-named session' {
        $SessionName = 'same-name-' + [Guid]::NewGuid().ToString()
        $EventName = $SessionName + '_TMSession.TokenRefreshTimer.Elapsed_*'
        $FirstSession = [TMSession]::new($SessionName)
        $SecondSession = [TMSession]::new($SessionName)

        try {
            @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 2
            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 2

            $FirstSession.Dispose()

            @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 1
            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 1
        } finally {
            $FirstSession.Dispose()
            $SecondSession.Dispose()
        }
    }

    It 'removes its event job when its subscriber was already unregistered' {
        $SessionName = 'unregistered-' + [Guid]::NewGuid().ToString()
        $EventName = $SessionName + '_TMSession.TokenRefreshTimer.Elapsed_*'
        $Session = [TMSession]::new($SessionName)

        try {
            Get-EventSubscriber -SourceIdentifier $EventName | Unregister-Event -Force
            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 1

            $Session.Dispose()

            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 0
        } finally {
            $Session.Dispose()
        }
    }

    It 'releases refresh resources after an expired-token refresh failure' {
        $SessionName = 'refresh-failure-' + [Guid]::NewGuid().ToString()
        $EventName = $SessionName + '_TMSession.TokenRefreshTimer.Elapsed_*'
        $Session = [TMSession]::new($SessionName)

        try {
            $Session.Authentication.OAuth.ExpirationSeconds = 1
            $Session.Authentication.OAuth.GrantedDate = (Get-Date).AddMinutes(-1)

            $Session.RefreshAccessToken()

            @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 0
            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 0
            $Session.Authentication.OAuth.tokenRefreshTimer | Should -BeNullOrEmpty
        } finally {
            $Session.Dispose()
        }
    }
}

Describe 'Remove-TMSession resource disposal' {
    It 'disposes a successfully removed session' {
        $SessionName = 'remove-success-' + [Guid]::NewGuid().ToString()
        $EventName = $SessionName + '_TMSession.TokenRefreshTimer.Elapsed_*'
        $Session = [TMSession]::new($SessionName, 'remove-success.transitionmanager.net', $true)
        $Global:TMSessions[$SessionName] = $Session
        Mock Invoke-WebRequest -ModuleName TransitionManager {
            [PSCustomObject]@{
                StatusCode = 200
                Content = '{"status":"success"}'
            }
        }

        try {
            $Session | Remove-TMSession

            $Global:TMSessions.ContainsKey($SessionName) | Should -BeFalse
            @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 0
            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 0
            $Session.Authentication.OAuth.tokenRefreshTimer | Should -BeNullOrEmpty
        } finally {
            $Session.Dispose()
            $Global:TMSessions.Remove($SessionName)
        }
    }

    It 'preserves a session and its resources when sign-out fails' {
        $SessionName = 'remove-failure-' + [Guid]::NewGuid().ToString()
        $EventName = $SessionName + '_TMSession.TokenRefreshTimer.Elapsed_*'
        $Session = [TMSession]::new($SessionName, 'remove-failure.transitionmanager.net', $true)
        $Global:TMSessions[$SessionName] = $Session
        Mock Invoke-WebRequest -ModuleName TransitionManager { throw 'Expected sign-out failure' }

        try {
            $Session | Remove-TMSession -ErrorAction SilentlyContinue

            $Global:TMSessions.ContainsKey($SessionName) | Should -BeTrue
            @(Get-EventSubscriber -SourceIdentifier $EventName -ErrorAction Ignore).Count | Should -Be 1
            @(Get-Job -Name $EventName -ErrorAction Ignore).Count | Should -Be 1
            $Session.Authentication.OAuth.tokenRefreshTimer.Enabled | Should -BeTrue
        } finally {
            $Session.Dispose()
            $Global:TMSessions.Remove($SessionName)
        }
    }
}