Tests/CP/CPAccount.Tests.ps1

BeforeAll {
    if ($Module) {
        $script:DevModuleName = Import-Module $Module -Force -PassThru | Select-Object -ExpandProperty Name
    }
    else {
        $devModule = Join-Path $PSScriptRoot '..' '..' 'Dev.Logic.Monitor.psd1'
        $script:DevModuleName = Import-Module $devModule -Force -PassThru | Select-Object -ExpandProperty Name
    }

    . (Join-Path $PSScriptRoot '..' 'Initialize-LMTestSecretManagement.ps1')
}

Describe 'Catchpoint auth cmdlets' {
    BeforeEach {
        Disconnect-CPAccount
    }

    It 'Sets CP auth state from a bearer token' {
        Connect-CPAccount -BearerToken 'test-token' -SkipCredValidation

        $status = Get-CPAccountStatus

        $status.Valid | Should -Be $true
        $status.PortalUrl | Should -Be 'https://io.catchpoint.com/api'
        $status.Type | Should -Be 'Bearer'
    }

    It 'Stores an optional account name' {
        Connect-CPAccount -BearerToken 'test-token' -AccountName 'prod' -SkipCredValidation

        $status = Get-CPAccountStatus

        $status.AccountName | Should -Be 'prod'
    }

    It 'Uses a custom API base URL' {
        Connect-CPAccount -BearerToken 'test-token' -ApiBaseUrl 'https://io.catchpoint.com/api/' -SkipCredValidation

        $status = Get-CPAccountStatus

        $status.PortalUrl | Should -Be 'https://io.catchpoint.com/api'
    }

    It 'Clears auth state on disconnect' {
        Connect-CPAccount -BearerToken 'test-token' -SkipCredValidation
        Disconnect-CPAccount

        Get-CPAccountStatus | Should -Be 'Not currently connected to any Catchpoint account.'
    }

    It 'Stores console logging preference when DisableConsoleLogging is used' {
        Connect-CPAccount -BearerToken 'test-token' -SkipCredValidation -DisableConsoleLogging

        $status = Get-CPAccountStatus

        $status.Logging | Should -Be $false
        InModuleScope -ModuleName $script:DevModuleName {
            $Script:InformationPreference | Should -Be 'SilentlyContinue'
        }
    }

    It 'Connects using a cached account by name' {
        Initialize-LMTestSecretManagementMocks
        Mock Get-SecretInfo -ModuleName $script:DevModuleName {
            [PSCustomObject]@{
                Name     = 'CP:prod'
                Metadata = @{
                    Portal    = 'prod'
                    PortalUrl = 'https://io.catchpoint.com/api'
                    Id        = 'prod'
                    Type      = 'CP'
                }
            }
        }
        Mock Get-Secret -ModuleName $script:DevModuleName {
            'cached-token' | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
        }

        Connect-CPAccount -CachedAccountName 'CP:prod' -SkipCredValidation

        $status = Get-CPAccountStatus
        $status.AccountName | Should -Be 'prod'
        $status.Type | Should -Be 'Cached'

        InModuleScope -ModuleName $script:DevModuleName {
            $token = [System.Net.NetworkCredential]::new('', $Script:CPAuth.BearerToken).Password
            $token | Should -Be 'cached-token'
        }
    }

    It 'Connects using normalized selection numbers from -UseCachedCredential' {
        Initialize-LMTestSecretManagementMocks
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Get-SecretInfo {
                @(
                    [PSCustomObject]@{
                        Name     = 'CP:first'
                        Metadata = @{
                            Portal = 'first'
                            Type   = 'CP'
                        }
                    }
                    [PSCustomObject]@{
                        Name     = 'commercial-account'
                        Metadata = @{
                            Portal = 'company'
                            Id     = 'lm-id'
                            Type   = 'LMv1'
                        }
                    }
                    [PSCustomObject]@{
                        Name     = 'CP:second'
                        Metadata = @{
                            Portal    = 'second'
                            PortalUrl = 'https://io.catchpoint.com/api'
                            Type      = 'CP'
                        }
                    }
                )
            }
            Mock Get-Secret {
                'second-token' | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
            }
            Mock Read-Host { '1' }

            Connect-CPAccount -UseCachedCredential -SkipCredValidation

            $status = Get-CPAccountStatus
            $status.AccountName | Should -Be 'second'
        }
    }

    It 'Validates credentials via GET /v2/products when SkipCredValidation is not set' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Invoke-RestMethod {
                return [PSCustomObject]@{
                    data      = @()
                    completed = $true
                }
            }

            { Connect-CPAccount -BearerToken 'test-token' } | Should -Not -Throw

            Should -Invoke Invoke-RestMethod -Times 1 -ParameterFilter {
                $Uri -eq 'https://io.catchpoint.com/api/v2/products' -and
                $Method -eq 'GET' -and
                $Headers.Authorization -eq 'Bearer test-token'
            }
        }
    }

    It 'Fails connect when the API returns HTTP 401' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Invoke-RestMethod {
                $httpResponse = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::Unauthorized)
                $httpResponse.Content = [System.Net.Http.StringContent]::new(
                    '{"errors":[{"message":"Invalid credentials"}],"traceId":"7eec5062-abcd-efgh-ijkl-1234567890ab"}',
                    [System.Text.Encoding]::UTF8,
                    'application/json'
                )

                $exception = [Microsoft.PowerShell.Commands.HttpResponseException]::new(
                    'Response status code does not indicate success: 401 (Unauthorized).',
                    $httpResponse
                )

                throw [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'InvokeRestMethod',
                    [System.Management.Automation.ErrorCategory]::InvalidOperation,
                    $null
                )
            }

            $errorRecord = { Connect-CPAccount -BearerToken 'bad-token' } |
                Should -Throw -ExpectedMessage '*Invalid Catchpoint credentials*' -PassThru

            $errorRecord.FullyQualifiedErrorId | Should -Be 'CP.AuthenticationError,Connect-CPAccount'
            $errorRecord.CategoryInfo.Category | Should -Be 'AuthenticationError'
        }
    }

    It 'Includes trace id in invalid credential errors when Catchpoint returns one' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Invoke-RestMethod {
                $httpResponse = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::Unauthorized)
                $httpResponse.Content = [System.Net.Http.StringContent]::new(
                    '{"errors":[{"message":"Expired Token"}],"traceId":"7eec5062-abcd-efgh-ijkl-1234567890ab"}',
                    [System.Text.Encoding]::UTF8,
                    'application/json'
                )

                $exception = [Microsoft.PowerShell.Commands.HttpResponseException]::new(
                    'Response status code does not indicate success: 401 (Unauthorized).',
                    $httpResponse
                )

                throw [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'InvokeRestMethod',
                    [System.Management.Automation.ErrorCategory]::InvalidOperation,
                    $null
                )
            }

            { Connect-CPAccount -BearerToken 'bad-token' } |
                Should -Throw '*Invalid Catchpoint credentials*trace id: 7eec5062-abcd-efgh-ijkl-1234567890ab*'
        }
    }

    It 'Skips the API check when SkipCredValidation is set' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Invoke-RestMethod { throw 'Should not be called' }

            { Connect-CPAccount -BearerToken 'test-token' -SkipCredValidation } | Should -Not -Throw

            Should -Invoke Invoke-RestMethod -Times 0 -Exactly
        }
    }
}

Describe 'Catchpoint private helpers' {
    It 'Builds a Bearer auth header' {
        InModuleScope -ModuleName $script:DevModuleName {
            $auth = [PSCustomObject]@{
                BearerToken = 'bearer-token' | ConvertTo-SecureString -AsPlainText -Force
                PortalUrl   = 'https://io.catchpoint.com/api'
            }

            $headers = New-CPHeader -Auth $auth

            $headers.Authorization | Should -Be 'Bearer bearer-token'
            $headers.'Content-Type' | Should -Be 'application/json'
            $headers.Accept | Should -Be 'application/json'
            $headers.'__CPMethod' | Should -Be 'GET'
        }
    }

    It 'Joins portal URL and resource path' {
        InModuleScope -ModuleName $script:DevModuleName {
            Join-CPUri -PortalUrl 'https://io.catchpoint.com/api/' -ResourcePath '/v2/products' |
                Should -Be 'https://io.catchpoint.com/api/v2/products'
        }
    }

    It 'Requires an active Catchpoint connection' {
        Disconnect-CPAccount

        InModuleScope -ModuleName $script:DevModuleName {
            Test-CPAuth -ErrorAction SilentlyContinue | Should -Be $false
        }
    }

    It 'Formats Catchpoint API error messages with trace id' {
        InModuleScope -ModuleName $script:DevModuleName {
            $message = Format-CPErrorMessage -ResponseBody '{"errors":[{"message":"Invalid credentials"}],"traceId":"abc-123"}' -StatusCode 401

            $message | Should -Be '401: Invalid credentials (trace id: abc-123)'
        }
    }

    It 'Returns non-JSON response bodies unchanged' {
        InModuleScope -ModuleName $script:DevModuleName {
            Format-CPErrorMessage -ResponseBody 'Gateway Timeout' -StatusCode 504 | Should -Be 'Gateway Timeout'
        }
    }
}

Describe 'CPCachedAccount cmdlets' {
    It 'Filters Catchpoint cached accounts by metadata type' {
        Mock Get-SecretInfo -ModuleName $script:DevModuleName {
            @(
                [PSCustomObject]@{
                    Name     = 'CP:prod'
                    Metadata = @{
                        Portal    = 'prod'
                        PortalUrl = 'https://io.catchpoint.com/api'
                        Modified  = Get-Date
                        Type      = 'CP'
                    }
                }
                [PSCustomObject]@{
                    Name     = 'commercial-account'
                    Metadata = @{
                        Portal   = 'company'
                        Id       = 'lm-id'
                        Modified = Get-Date
                        Type     = 'LMv1'
                    }
                }
            )
        }

        $accounts = Get-CPCachedAccount

        $accounts.Count | Should -Be 1
        $accounts[0].CachedAccountName | Should -Be 'CP:prod'
        $accounts[0].AccountName | Should -Be 'prod'
    }
}