Public/Tests/Confluence.Additions.Tests.ps1

BeforeAll {
    $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config'
    $env:TCS_SKIP_UPDATE_CHECK = '1'
    $env:TCS_TELEMETRY_OPTOUT = '1'
    $ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
    Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.confluence.psd1') -Force
}

AfterAll {
    Remove-Module -Name tcs.confluence -Force -ErrorAction SilentlyContinue
}

Describe 'REST additions' {
    BeforeAll {
        function Get-DecodedUri($Uri) { [System.Net.WebUtility]::UrlDecode($Uri.OriginalString) }
    }

    BeforeEach {
        Set-ConfluenceContext -ConfluenceUrl 'https://contoso.atlassian.net' -Username 'user@contoso.com' -PersonalAccessToken 'token'
        Mock -ModuleName tcs.confluence Invoke-WebRequest {
            [pscustomobject]@{ StatusCode = 200; StatusDescription = 'OK'; Content = '{"results":[{"id":"1","title":"A","name":"a"}]}' }
        }
    }

    Context 'Search-ConfluenceContent' {
        It 'Sends the CQL query to /wiki/rest/api/search and writes the results' {
            $results = @(Search-ConfluenceContent -Cql 'type = page AND label = "run book"' -Limit 50 -Expand content.space)
            $results.Count | Should -Be 1
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter {
                (Get-DecodedUri $Uri) -eq 'https://contoso.atlassian.net/wiki/rest/api/search?cql=type = page AND label = "run book"&expand=content.space&limit=50'
            }
        }

        It 'Follows v1 next links relative to /wiki with -All' {
            Mock -ModuleName tcs.confluence Invoke-WebRequest {
                if ($Uri.OriginalString -like '*cursor=2*') {
                    return [pscustomobject]@{ StatusCode = 200; StatusDescription = 'OK'; Content = '{"results":[{"title":"B"}],"_links":{}}' }
                }
                [pscustomobject]@{ StatusCode = 200; StatusDescription = 'OK'; Content = '{"results":[{"title":"A"}],"_links":{"next":"/rest/api/search?cql=x&cursor=2"}}' }
            }
            (Search-ConfluenceContent -Cql 'x' -MaxQueryPages 1 -All).title | Should -Be @('A', 'B')
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter { $Uri.OriginalString -eq 'https://contoso.atlassian.net/wiki/rest/api/search?cql=x&cursor=2' }
        }
    }

    Context 'Get-ConfluencePageChild' {
        It 'Gets the direct children from the v2 children endpoint' {
            $children = @(Get-ConfluencePageChild -PageId 10)
            $children.id | Should -Be @('1')
            $children[0].parentId | Should -Be '10'
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter { $Uri.OriginalString -eq 'https://contoso.atlassian.net/wiki/api/v2/pages/10/children?limit=250' }
        }

        It 'Returns every descendant with -Recurse' {
            Mock -ModuleName tcs.confluence Invoke-WebRequest {
                $content = switch -Wildcard ($Uri.OriginalString) {
                    '*/pages/10/children*' { '{"results":[{"id":"11"},{"id":"12"}]}' }
                    '*/pages/11/children*' { '{"results":[{"id":"111"}]}' }
                    default { '{"results":[]}' }
                }
                [pscustomobject]@{ StatusCode = 200; StatusDescription = 'OK'; Content = $content }
            }
            $pages = @(Get-ConfluencePageChild -PageId 10 -Recurse)
            $pages.id | Should -Be @('11', '12', '111')
            ($pages | Where-Object id -EQ '111').parentId | Should -Be '11'
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 4 -Exactly
        }
    }

    Context 'Labels' {
        It 'Gets labels from the v2 labels endpoint' {
            $labels = @(Get-ConfluencePageLabel -PageId 5 -Prefix global)
            $labels.name | Should -Be @('a')
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter { $Uri.OriginalString -eq 'https://contoso.atlassian.net/wiki/api/v2/pages/5/labels?limit=250&prefix=global' }
        }

        It 'Adds labels with one POST of a JSON array' {
            $null = Add-ConfluencePageLabel -PageId 5 -Label 'ops'
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter {
                $json = [System.Text.Encoding]::UTF8.GetString($Body)
                $Method -eq 'POST' -and $Uri.OriginalString -eq 'https://contoso.atlassian.net/wiki/rest/api/content/5/label' -and
                $json -eq '[{"prefix":"global","name":"ops"}]'
            }
        }

        It 'Rejects labels with spaces and sends nothing with -WhatIf' {
            { Add-ConfluencePageLabel -PageId 5 -Label 'two words' } | Should -Throw
            Add-ConfluencePageLabel -PageId 5 -Label ops -WhatIf
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 0 -Exactly
        }

        It 'Removes each label with DELETE ...label?name=' {
            Mock -ModuleName tcs.confluence Invoke-WebRequest { [pscustomobject]@{ StatusCode = 204; StatusDescription = 'No Content'; Content = '' } }
            [pscustomobject]@{ id = '5' } | Remove-ConfluencePageLabel -Label 'a b/c', 'draft' -Confirm:$false
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter {
                $Method -eq 'DELETE' -and $Uri.OriginalString -eq 'https://contoso.atlassian.net/wiki/rest/api/content/5/label?name=a+b%2Fc'
            }
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter { $Uri.OriginalString -like '*label?name=draft' }
        }
    }

    Context 'Attachments' {
        It 'Lists attachments from the v2 attachments endpoint' {
            $null = Get-ConfluenceAttachment -PageId 5 -FileName 'a b.pdf'
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter { (Get-DecodedUri $Uri) -eq 'https://contoso.atlassian.net/wiki/api/v2/pages/5/attachments?filename=a b.pdf&limit=250' }
        }

        It 'Uploads a file as multipart/form-data with the no-check header' {
            $file = Join-Path -Path $TestDrive -ChildPath 'report.txt'
            [System.IO.File]::WriteAllText($file, 'hello')
            $result = @(Add-ConfluenceAttachment -PageId 5 -Path $file -Comment 'nightly')
            $result.Count | Should -Be 1
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 1 -Exactly -ParameterFilter {
                $text = [System.Text.Encoding]::UTF8.GetString($Body)
                $Method -eq 'PUT' -and $Uri.OriginalString -eq 'https://contoso.atlassian.net/wiki/rest/api/content/5/child/attachment' -and
                $ContentType -like 'multipart/form-data; boundary=*' -and $Headers['X-Atlassian-Token'] -eq 'no-check' -and
                $text -match 'name="file"; filename="report.txt"' -and $text -match "`r`n`r`nhello`r`n" -and
                $text -match 'name="comment"' -and $text -match 'name="minorEdit"\r\n\r\ntrue'
            }
        }

        It 'Reports a missing file and uploads nothing' {
            Add-ConfluenceAttachment -PageId 5 -Path (Join-Path -Path $TestDrive -ChildPath 'missing.bin') -ErrorAction SilentlyContinue -ErrorVariable uploadError
            "$uploadError" | Should -BeLike '*File not found*'
            Should -Invoke -ModuleName tcs.confluence Invoke-WebRequest -Times 0 -Exactly
        }

        It 'Reports HTTP errors' {
            Mock -ModuleName tcs.confluence Invoke-WebRequest { [pscustomobject]@{ StatusCode = 403; StatusDescription = 'Forbidden'; Content = '{"message":"no permission"}' } }
            $file = Join-Path -Path $TestDrive -ChildPath 'x.txt'
            [System.IO.File]::WriteAllText($file, 'x')
            { Add-ConfluenceAttachment -PageId 5 -Path $file -ErrorAction Stop } | Should -Throw -ExpectedMessage '*403*no permission*'
        }
    }

    Context 'Clear-ConfluenceContext' {
        It 'Forgets the context and credential' {
            Clear-ConfluenceContext
            Get-ConfluenceContext | Should -BeNullOrEmpty
            InModuleScope tcs.confluence { $script:ConfluenceCredential } | Should -BeNullOrEmpty
            { Get-ConfluencePage -PageId 1 -ErrorAction Stop } | Should -Throw -ExpectedMessage '*Set-ConfluenceContext*'
        }

        It 'Changes nothing with -WhatIf' {
            Clear-ConfluenceContext -WhatIf
            Get-ConfluenceContext | Should -Not -BeNullOrEmpty
        }
    }
}