Tests/EAI/EAIAccessToken.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
    }
}

Describe 'Request-EAIAccessToken' {
    It 'Requests a token using client credentials' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Invoke-RestMethod {
                return [PSCustomObject]@{
                    access_token = 'issued-token'
                    token_type   = 'Bearer'
                    expires_in   = 1800
                }
            }

            $result = Request-EAIAccessToken -EdwinOrg 'myorg' -ClientId 'client' -ClientSecret ('secret' | ConvertTo-SecureString -AsPlainText -Force)

            Should -Invoke Invoke-RestMethod -Times 1 -ParameterFilter {
                $Uri -eq 'https://myorg.dexda.ai/auth/token' -and
                $Method -eq 'POST' -and
                $ContentType -eq 'application/x-www-form-urlencoded' -and
                $Body -match 'grant_type=client_credentials' -and
                $Body -match 'client_id=client' -and
                $Body -match 'client_secret=secret'
            }

            [System.Net.NetworkCredential]::new('', $result.AccessToken).Password | Should -Be 'issued-token'
            $result.ExpiresAt | Should -BeOfType [DateTime]
        }
    }

    It 'Parses expires_at epoch milliseconds when present' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Invoke-RestMethod {
                return [PSCustomObject]@{
                    access_token = 'issued-token'
                    expires_at   = 1759145595335
                }
            }

            $result = Request-EAIAccessToken -EdwinOrg 'myorg' -ClientId 'client' -ClientSecret ('secret' | ConvertTo-SecureString -AsPlainText -Force)

            $result.ExpiresAt | Should -Be ([DateTimeOffset]::FromUnixTimeMilliseconds(1759145595335).UtcDateTime)
        }
    }

    It 'Throws a connect-specific error for 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(
                    '{"code":401,"message":"Credentials are required to access this resource."}',
                    [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
                )
            }

            { Request-EAIAccessToken -EdwinOrg 'myorg' -ClientId 'client' -ClientSecret ('secret' | ConvertTo-SecureString -AsPlainText -Force) } |
                Should -Throw '*Invalid Edwin credentials for portal*myorg*'
        }
    }
}

Describe 'Get-EAIBearerToken' {
    It 'Returns a cached token when it expires more than 60 seconds from now' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Request-EAIAccessToken { throw 'Should not refresh' }

            $auth = [PSCustomObject]@{
                EdwinOrg       = 'myorg'
                ClientId       = 'client'
                ClientSecret   = 'secret' | ConvertTo-SecureString -AsPlainText -Force
                AccessToken    = 'cached-token' | ConvertTo-SecureString -AsPlainText -Force
                TokenExpiresAt = (Get-Date).ToUniversalTime().AddMinutes(10)
            }

            Get-EAIBearerToken -Auth $auth | Should -Be 'cached-token'
            Should -Invoke Request-EAIAccessToken -Times 0 -Exactly
        }
    }

    It 'Refreshes the token when it is near expiry' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Request-EAIAccessToken {
                return [PSCustomObject]@{
                    AccessToken = ('fresh-token' | ConvertTo-SecureString -AsPlainText -Force)
                    ExpiresAt   = (Get-Date).ToUniversalTime().AddHours(1)
                }
            }

            $auth = [PSCustomObject]@{
                EdwinOrg       = 'myorg'
                ClientId       = 'client'
                ClientSecret   = 'secret' | ConvertTo-SecureString -AsPlainText -Force
                AccessToken    = 'stale-token' | ConvertTo-SecureString -AsPlainText -Force
                TokenExpiresAt = (Get-Date).ToUniversalTime().AddSeconds(30)
            }

            Get-EAIBearerToken -Auth $auth | Should -Be 'fresh-token'
            Should -Invoke Request-EAIAccessToken -Times 1 -Exactly
        }
    }

    It 'Forces a refresh when requested' {
        InModuleScope -ModuleName $script:DevModuleName {
            Mock Request-EAIAccessToken {
                return [PSCustomObject]@{
                    AccessToken = ('forced-token' | ConvertTo-SecureString -AsPlainText -Force)
                    ExpiresAt   = (Get-Date).ToUniversalTime().AddHours(1)
                }
            }

            $auth = [PSCustomObject]@{
                EdwinOrg       = 'myorg'
                ClientId       = 'client'
                ClientSecret   = 'secret' | ConvertTo-SecureString -AsPlainText -Force
                AccessToken    = 'cached-token' | ConvertTo-SecureString -AsPlainText -Force
                TokenExpiresAt = (Get-Date).ToUniversalTime().AddHours(1)
            }

            Get-EAIBearerToken -Auth $auth -ForceRefresh | Should -Be 'forced-token'
            Should -Invoke Request-EAIAccessToken -Times 1 -Exactly
        }
    }
}