tests/RTShell.Tests.Integration.ps1

#Requires -Module Pester
<#
.SYNOPSIS
    RTShell integration tests — requires a live RT instance.
 
.DESCRIPTION
    These tests exercise the full stack against a real RT server. They create,
    modify, and resolve actual tickets, then clean up after themselves.
 
    PREREQUISITES
    ─────────────
    1. A running RT 4.4+ instance accessible from this machine.
    2. An RT API token with permission to create/modify tickets in the test queue.
    3. The RTShell module imported and a session established, OR the environment
       variables below set so the test file can call Connect-RT itself.
 
    CONFIGURATION (environment variables)
    ──────────────────────────────────────
    RT_BASE_URI – Base URL of the RT instance, e.g. https://rt.example.com
    RT_API_TOKEN – API token (plain text)
    RT_TEST_QUEUE – Queue to use for test tickets (default: General)
 
    RUNNING
    ───────
    # Set env vars then run:
    $env:RT_BASE_URI = 'https://rt.example.com'
    $env:RT_API_TOKEN = 'your-token-here'
    Invoke-Pester .\RTShell.Tests.Integration.ps1 -Output Detailed
 
    WARNING: These tests create real tickets in your RT instance. Use a
    dedicated test queue and confirm the cleanup AfterAll block runs.
#>


BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..' 'RTShell.psd1'
    if (-not (Test-Path $modulePath)) {
        $modulePath = Join-Path $PSScriptRoot 'RTShell.psd1'
    }
    Import-Module $modulePath -Force -ErrorAction Stop

    # ── Configuration ─────────────────────────────────────────────────────────
    # Prefer environment variables; fall back to interactive prompts so the
    # test file works in both CI (env vars pre-set) and local dev (interactive).
    $script:BaseUri = $env:RT_BASE_URI
    $script:Token = $env:RT_API_TOKEN
    $script:TestQueue = $env:RT_TEST_QUEUE
    $script:Requestor = $env:RT_TEST_REQUESTOR

    if (-not $script:BaseUri) {
        $script:BaseUri = Read-Host 'RT Base URI (e.g. https://rt.example.com)'
    }
    if (-not $script:Token) {
        $script:Token = Read-Host 'RT API Token'
    }
    if (-not $script:TestQueue) {
        $script:TestQueue = Read-Host 'RT Queue for test tickets (default: General) [press Enter to accept]'
        if (-not $script:TestQueue) { $script:TestQueue = 'General' }
    }
    if (-not $script:Requestor) {
        $script:Requestor = Read-Host 'Requestor email for test tickets (e.g. testuser@example.com)'
    }

    # Connect
    Connect-RT -BaseUri $script:BaseUri -TokenPlainText $script:Token

    # Track all tickets created so AfterAll can resolve them.
    $script:CreatedTicketIds = [System.Collections.Generic.List[int]]::new()
}

AfterAll {
    Write-Output "`n[Cleanup] Resolving $($script:CreatedTicketIds.Count) test ticket(s)..." -ForegroundColor Yellow
    foreach ($id in $script:CreatedTicketIds) {
        try {
            Set-RTTicketStatus -Id $id -Status resolved -Force
            Write-Output " Resolved ticket #$id" -ForegroundColor Gray
        }
        catch {
            Write-Warning "Could not resolve ticket #$id`: $_"
        }
    }
    Disconnect-RT
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Connect-RT / Disconnect-RT' {

    It 'Connects successfully and reports the correct base URI' {
        # Already connected in BeforeAll — just verify the session state.
        InModuleScope RTShell {
            $Script:RTSession.Connected | Should -BeTrue
            $Script:RTSession.BaseUri | Should -Not -BeNullOrEmpty
        }
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Get-RTQueue' {

    It 'Returns at least one queue' {
        $queues = @(Get-RTQueue)
        $queues.Count | Should -BeGreaterOrEqual 1
    }

    It 'Each queue has a Name and Id' {
        $queue = Get-RTQueue | Select-Object -First 1
        $queue.Name | Should -Not -BeNullOrEmpty
        $queue.Id | Should -BeGreaterThan 0
    }

    It 'Returns a queue when filtered by name (substring)' {
        # Grab the first queue name and search for a substring of it
        $firstName = (Get-RTQueue | Select-Object -First 1).Name
        $substring = $firstName.Substring(0, [Math]::Max(1, $firstName.Length - 1))
        $result = @(Get-RTQueue -Name $substring)
        $result.Count | Should -BeGreaterOrEqual 1
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'New-RTTicket' {

    It 'Creates a ticket and returns an ID' {
        $ticket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] New-RTTicket basic' `
            -Force -PassThru

        $script:CreatedTicketIds.Add($ticket.Id)

        $ticket.Id | Should -BeGreaterThan 0
        $ticket.Subject | Should -Be '[RTShell Test] New-RTTicket basic'
        $ticket.Queue | Should -Be $script:TestQueue
    }

    It 'Creates a ticket with an initial body' {
        $ticket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] New-RTTicket with body' `
            -Body 'This is the initial message body.' `
            -Force -PassThru

        $script:CreatedTicketIds.Add($ticket.Id)
        $ticket.Id | Should -BeGreaterThan 0
    }

    It 'Creates a ticket with a non-default status' {
        # Use 'open' rather than 'stalled' — many RT queues reject 'stalled'
        # as an initial status via a lifecycle policy, even though the API
        # parameter is valid. 'open' is universally accepted at creation.
        $ticket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] New-RTTicket open status' `
            -Status open `
            -Force -PassThru

        $script:CreatedTicketIds.Add($ticket.Id)
        $ticket.Status | Should -Be 'open'
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Get-RTTicket' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Get-RTTicket' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:GetTestTicketId = $newTicket.Id
    }

    It 'Returns a ticket by ID' {
        $ticket = Get-RTTicket -Id $script:GetTestTicketId
        $ticket.Id | Should -Be $script:GetTestTicketId
        $ticket.Subject | Should -Be '[RTShell Test] Get-RTTicket'
    }

    It 'Returns multiple tickets by ID' {
        $ids = @($script:GetTestTicketId)
        $results = @(Get-RTTicket -Id $ids)
        $results.Count | Should -Be $ids.Count
    }

    It 'Returns detailed fields with -Detailed' {
        $ticket = Get-RTTicket -Id $script:GetTestTicketId -Detailed
        $ticket.PSObject.Properties.Name | Should -Contain 'Priority'
        $ticket.PSObject.Properties.Name | Should -Contain 'CustomFields'
    }

    It 'Accepts pipeline input from Search-RTTicket' {
        $result = Search-RTTicket -Query "id=$($script:GetTestTicketId)" |
            Get-RTTicket
        $result.Id | Should -Be $script:GetTestTicketId
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Search-RTTicket' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Search-RTTicket unique marker xq7z' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:SearchTestId = $newTicket.Id
    }

    It 'Finds a ticket by unique keyword in subject' {
        $results = @(Search-RTTicket -Keyword 'xq7z' -Status any)
        $results.Count | Should -BeGreaterOrEqual 1
        ($results | Where-Object { $_.Id -eq $script:SearchTestId }) | Should -Not -BeNullOrEmpty
    }

    It 'Returns results with the expected properties' {
        $result = Search-RTTicket -Keyword 'xq7z' -Status any | Select-Object -First 1
        $result.PSObject.Properties.Name | Should -Contain 'Id'
        $result.PSObject.Properties.Name | Should -Contain 'Subject'
        $result.PSObject.Properties.Name | Should -Contain 'Status'
    }

    It 'Respects -PageSize' {
        $results = @(Search-RTTicket -Status any -PageSize 2 -Page 1)
        $results.Count | Should -BeLessOrEqual 2
    }

    It 'Passes a raw TicketSQL query' {
        $results = @(Search-RTTicket -Query "id=$($script:SearchTestId)")
        $results.Count | Should -Be 1
        $results[0].Id | Should -Be $script:SearchTestId
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Set-RTTicketStatus' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Set-RTTicketStatus' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:StatusTestId = $newTicket.Id
    }

    It 'Changes status to stalled' {
        Set-RTTicketStatus -Id $script:StatusTestId -Status stalled -Force
        $ticket = Get-RTTicket -Id $script:StatusTestId
        $ticket.Status | Should -Be 'stalled'
    }

    It 'Changes status back to open' {
        Set-RTTicketStatus -Id $script:StatusTestId -Status open -Force
        $ticket = Get-RTTicket -Id $script:StatusTestId
        $ticket.Status | Should -Be 'open'
    }

    It 'Returns the updated ticket with -PassThru' {
        $result = Set-RTTicketStatus -Id $script:StatusTestId -Status stalled -Force -PassThru
        $result | Should -Not -BeNullOrEmpty
        $result.psobject.TypeNames[0] | Should -Be 'RTShell.Ticket'
        $result.Id | Should -Be $script:StatusTestId
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Set-RTTicketOwner' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Set-RTTicketOwner' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:OwnerTestId = $newTicket.Id

        # Determine the current authenticated username for the "take ownership" test.
        # The username is visible in the RT session token context — we can look it up
        # by fetching the 'user/root' or 'user/current' endpoint if your RT supports it.
        # Fallback: use the RT_TEST_OWNER env var if set.
        $script:TestOwner = if ($env:RT_TEST_OWNER) { $env:RT_TEST_OWNER } else { 'Nobody' }
    }

    It 'Sets the owner to Nobody (unassign)' {
        Set-RTTicketOwner -Id $script:OwnerTestId -Owner Nobody -Force
        $ticket = Get-RTTicket -Id $script:OwnerTestId
        # RT may return 'Nobody' or an empty string depending on version
        ($ticket.Owner -eq 'Nobody' -or [string]::IsNullOrEmpty($ticket.Owner)) | Should -BeTrue
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Set-RTTicketPriority' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Set-RTTicketPriority' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:PriorityTestId = $newTicket.Id
    }

    It 'Sets priority to 80' {
        Set-RTTicketPriority -Id $script:PriorityTestId -Priority 80 -Force
        $ticket = Get-RTTicket -Id $script:PriorityTestId -Detailed
        [int]$ticket.Priority | Should -Be 80
    }

    It 'Sets priority to 0' {
        Set-RTTicketPriority -Id $script:PriorityTestId -Priority 0 -Force
        $ticket = Get-RTTicket -Id $script:PriorityTestId -Detailed
        [int]$ticket.Priority | Should -Be 0
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Set-RTTicketField' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Set-RTTicketField original' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:FieldTestId = $newTicket.Id
    }

    It 'Updates the Subject via -Fields' {
        Set-RTTicketField -Id $script:FieldTestId `
            -Fields @{ Subject = '[RTShell Test] Set-RTTicketField updated' } -Force
        $ticket = Get-RTTicket -Id $script:FieldTestId
        $ticket.Subject | Should -Be '[RTShell Test] Set-RTTicketField updated'
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Add-RTTicketComment' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Add-RTTicketComment' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:CommentTestId = $newTicket.Id
    }

    It 'Posts a comment without error' {
        { Add-RTTicketComment -Id $script:CommentTestId -Body 'Integration test comment.' -Force } |
            Should -Not -Throw
    }

    It 'Comment appears in ticket history' {
        $history = @(Get-RTTicketHistory -Id $script:CommentTestId -Type Comment)
        $history.Count | Should -BeGreaterOrEqual 1
        ($history | Where-Object { $_.Content -match 'Integration test comment' }) | Should -Not -BeNullOrEmpty
    }

    It 'Returns the updated ticket with -PassThru' {
        $result = Add-RTTicketComment -Id $script:CommentTestId -Body 'PassThru test.' -Force -PassThru
        $result | Should -Not -BeNullOrEmpty
        $result.psobject.TypeNames[0] | Should -Be 'RTShell.Ticket'
        $result.Id | Should -Be $script:CommentTestId
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Add-RTTicketReply' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Add-RTTicketReply' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:ReplyTestId = $newTicket.Id
    }

    It 'Posts a reply without error' {
        { Add-RTTicketReply -Id $script:ReplyTestId -Body 'Integration test reply.' -Force } |
            Should -Not -Throw
    }

    It 'Reply appears in ticket history as Correspond type' {
        $history = @(Get-RTTicketHistory -Id $script:ReplyTestId -Detailed)
        $correspond = $history | Where-Object { $_.Type -eq 'Correspond' -and $_.Content -match 'Integration test reply' }
        $correspond | Should -Not -BeNullOrEmpty
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Add-RTTicketAttachment' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Add-RTTicketAttachment' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:AttachTestId = $newTicket.Id

        # Create a small temp file to upload
        $script:TempFile = Join-Path $TestDrive 'rtshell_test_attach.txt'
        Set-Content -Path $script:TempFile -Value 'RTShell attachment integration test payload.'
    }

    It 'Uploads a file without error' {
        { Add-RTTicketAttachment -Id $script:AttachTestId -Path $script:TempFile -Force } |
            Should -Not -Throw
    }

    It 'Attachment appears in Get-RTTicketAttachments' {
        $attachments = @(Get-RTTicketAttachments -Id $script:AttachTestId)
        $match = $attachments | Where-Object { $_.Filename -eq 'rtshell_test_attach.txt' }
        $match | Should -Not -BeNullOrEmpty
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Get-RTTicketHistory' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Get-RTTicketHistory' `
            -Body 'Initial create body for history test.' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:HistoryTestId = $newTicket.Id

        Add-RTTicketComment -Id $script:HistoryTestId -Body 'History comment 1.' -Force
        Add-RTTicketReply -Id $script:HistoryTestId -Body 'History reply 1.' -Force
    }

    It 'Returns history items' {
        $history = @(Get-RTTicketHistory -Id $script:HistoryTestId)
        $history.Count | Should -BeGreaterOrEqual 1
    }

    It 'Filters to Correspond type' {
        $history = @(Get-RTTicketHistory -Id $script:HistoryTestId -Type Correspond)
        # $nonCorrespond = $history | Where-Object { $_.PSTypeName -ne 'RTShell.TicketHistory.Summary' }
        # All returned items should be from correspond transactions
        $history.Count | Should -BeGreaterOrEqual 1
    }

    It 'Returns TicketTransaction objects with -Detailed' {
        $history = @(Get-RTTicketHistory -Id $script:HistoryTestId -Detailed)
        $history.Count | Should -BeGreaterOrEqual 1
        $history[0].PSObject.Properties.Name | Should -Contain 'TransactionId'
        $history[0].PSObject.Properties.Name | Should -Contain 'Type'
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Get-RTTicketAttachments / Save-RTTicketAttachment' {

    BeforeAll {
        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Attachments round-trip' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:RoundTripId = $newTicket.Id

        $script:UploadFile = Join-Path $TestDrive 'upload_roundtrip.txt'
        Set-Content -Path $script:UploadFile -Value 'Round-trip test content.'
        Add-RTTicketAttachment -Id $script:RoundTripId -Path $script:UploadFile -Force
    }

    It 'Get-RTTicketAttachments returns the uploaded file metadata' {
        $attachments = @(Get-RTTicketAttachments -Id $script:RoundTripId)
        ($attachments | Where-Object { $_.Filename -eq 'upload_roundtrip.txt' }) | Should -Not -BeNullOrEmpty
    }

    It 'Save-RTTicketAttachment downloads and writes the file' {
        $destDir = Join-Path $TestDrive 'downloads'
        $attachments = Get-RTTicketAttachments -Id $script:RoundTripId |
            Where-Object { $_.Filename -eq 'upload_roundtrip.txt' }

        $attachments | Save-RTTicketAttachment -DestinationPath $destDir -Force

        $outFile = Join-Path $destDir 'upload_roundtrip.txt'
        $outFile | Should -Exist
        (Get-Item $outFile).Length | Should -BeGreaterThan 0
        # Decode the file content regardless of whether RT returned it as
        # plain text or base64, and verify the original payload is present.
        $raw = Get-Content $outFile -Raw
        $decoded = try {
            [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(($raw -replace '\s', '')))
        }
        catch { $raw }
        $decoded | Should -Match 'Round-trip test content'
    }
}

# ══════════════════════════════════════════════════════════════════════════════
Describe 'Template round-trip with Add-RTTicketReply' {

    BeforeAll {
        # Create a temp template dir so we don't pollute real user templates.
        $script:IntegTempDir = Join-Path $TestDrive 'integ_templates'
        New-Item -ItemType Directory -Path $script:IntegTempDir -Force | Out-Null

        # Mock Get-RTTemplateDirectory using -ModuleName so it applies to
        # calls made inside the RTShell module. The mock scriptblock closes
        # over $script:IntegTempDir directly — no $using: needed because
        # BeforeAll runs in the same runspace as the It blocks.
        Mock -CommandName Get-RTTemplateDirectory `
            -ModuleName RTShell `
            -MockWith { $script:IntegTempDir }

        $newTicket = New-RTTicket `
            -Queue $script:TestQueue `
            -Subject '[RTShell Test] Template reply round-trip' `
            -Force -PassThru
        $script:CreatedTicketIds.Add($newTicket.Id)
        $script:TplReplyTestId = $newTicket.Id
    }

    It 'Creates a template, resolves it, and sends the reply' {
        New-RTTemplate -Name 'integ-test-tpl' `
            -Description 'Integration test template' `
            -Body 'Ticket {{TicketId}} is in queue {{Queue}}. Extra: {{MyToken}}' `
            -Prompts @{ MyToken = 'Enter value' } `
            -Confirm:$false

        { Add-RTTicketReply -Id $script:TplReplyTestId `
                -TemplateName 'integ-test-tpl' `
                -TemplateValues @{ MyToken = 'resolved_value' } `
                -Force } | Should -Not -Throw

        $history = @(Get-RTTicketHistory -Id $script:TplReplyTestId -Type Correspond)
        $match = $history | Where-Object { $_.Content -match 'resolved_value' }
        $match | Should -Not -BeNullOrEmpty
    }
}