Tests/Unit/RequestConstruction.Tests.ps1

BeforeAll {
    $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../..')
    Import-Module (Join-Path $repoRoot 'Pax8-API.psd1') -Force

    Add-Type -AssemblyName System.Web

    function Get-CapturedRequest {
        $script:CapturedRequests.ToArray()
    }

    function Get-QueryValue {
        param (
            [Parameter(Mandatory)]
            [string]$Uri,

            [Parameter(Mandatory)]
            [string]$Name
        )

        $builder = [System.UriBuilder]$Uri
        $query = [System.Web.HttpUtility]::ParseQueryString($builder.Query.TrimStart('?'))
        $query[$Name]
    }
}

Describe 'PAX8 request construction' {
    BeforeEach {
        InModuleScope 'Pax8-API' {
            $script:Pax8RestUrl = 'https://api.example.test/v1'
        }

        $script:CapturedRequests = [System.Collections.Generic.List[object]]::new()

        Mock -ModuleName 'Pax8-API' -CommandName Invoke-Pax8MasterRequest -MockWith {
            param ($Arguments)

            $script:CapturedRequests.Add($Arguments)
            [pscustomobject]@{
                ok = $true
            }
        }
    }

    It 'adds companyId as a query parameter for product pricing' {
        $companyId = [guid]::NewGuid()

        Get-Pax8ProductPricing -productId 'product-1' -companyId $companyId | Out-Null

        $request = Get-CapturedRequest
        $request.Method | Should -Be 'Get'
        $request.Uri | Should -Be "https://api.example.test:443/v1/products/product-1/pricing?companyId=$companyId"
    }

    It 'keeps contact list companyId in the path only' {
        $companyId = [guid]::NewGuid()

        Get-Pax8Contact -companyId $companyId -page 1 -size 10 | Out-Null

        $request = Get-CapturedRequest
        $request.Method | Should -Be 'Get'
        $request.Uri | Should -Be "https://api.example.test:443/v1/companies/$companyId/contacts?page=1&size=10"
        Get-QueryValue -Uri $request.Uri -Name 'companyId' | Should -BeNullOrEmpty
    }

    It 'omits path companyId from contact create body and serializes email as a string' {
        $companyId = [guid]::NewGuid()

        New-Pax8Contact `
            -companyId $companyId `
            -firstName 'John' `
            -lastName 'Doe' `
            -email 'john@example.com' `
            -phone '555-555-5555' `
            -Confirm:$false | Out-Null

        $request = Get-CapturedRequest
        $body = $request.Body | ConvertFrom-Json

        $request.Method | Should -Be 'Post'
        $request.Uri | Should -Be "https://api.example.test:443/v1/companies/$companyId/contacts"
        $body.PSObject.Properties.Name | Should -Not -Contain 'companyId'
        $body.email | Should -Be 'john@example.com'
        $body.email.GetType().FullName | Should -Be 'System.String'
    }

    It 'includes company contacts and order approval flag in company create body' {
        New-Pax8Company `
            -Name 'Initech' `
            -address @{ street = '4120 Freidrich Ln'; city = 'Austin' } `
            -phone '999-999-9999' `
            -website 'https://initech.example' `
            -billOnBehalfOfEnabled:$false `
            -selfServiceAllowed:$true `
            -orderApprovalRequired:$true `
            -contacts @(@{ firstName = 'Peter'; lastName = 'Gibbons'; email = 'pgibbons@example.com'; phone = '555-555-5555' }) `
            -Confirm:$false | Out-Null

        $request = Get-CapturedRequest
        $body = $request.Body | ConvertFrom-Json

        $request.Method | Should -Be 'Post'
        $request.Uri | Should -Be 'https://api.example.test:443/v1/companies'
        $body.orderApprovalRequired | Should -BeTrue
        $body.contacts[0].firstName | Should -Be 'Peter'
    }

    It 'uses PATCH and a partial body for company updates' {
        $companyId = [guid]::NewGuid()

        Update-Pax8CompanyById -companyId $companyId -orderApprovalRequired:$false -Confirm:$false | Out-Null

        $request = Get-CapturedRequest
        $body = $request.Body | ConvertFrom-Json

        $request.Method | Should -Be 'Patch'
        $request.Uri | Should -Be "https://api.example.test:443/v1/companies/$companyId"
        $body.PSObject.Properties.Name | Should -Contain 'orderApprovalRequired'
        $body.orderApprovalRequired | Should -BeFalse
    }

    It 'rejects company updates without update fields' {
        $companyId = [guid]::NewGuid()

        { Update-Pax8CompanyById -companyId $companyId -Confirm:$false } | Should -Throw 'At least one update field must be specified.'
    }

    It 'rejects subscription updates without update fields' {
        { Update-Pax8SubscriptionById -subscriptionId 'subscription-1' -Confirm:$false } | Should -Throw 'At least one update field must be specified.'
    }

    It 'passes product search through as a query parameter' {
        Get-Pax8Product -search 'backup' -page 1 -size 10 | Out-Null

        $request = Get-CapturedRequest

        $request.Method | Should -Be 'Get'
        $request.Uri | Should -Be 'https://api.example.test:443/v1/products?search=backup&page=1&size=10'
    }

    It 'builds draft invoice item query parameters' {
        $companyId = [guid]::NewGuid()

        Get-Pax8InvoiceDraftItem -companyId $companyId -monthOffset 1 -page 1 -size 10 | Out-Null

        $request = Get-CapturedRequest

        $request.Method | Should -Be 'Get'
        $request.Uri | Should -Be "https://api.example.test:443/v1/invoices/draftItems?companyId=$companyId&monthOffset=1&page=1&size=10"
    }

    It 'uses the documented draft invoice item month offset default' {
        Get-Pax8InvoiceDraftItem -page 1 -size 10 | Out-Null

        $request = Get-CapturedRequest

        Get-QueryValue -Uri $request.Uri -Name 'monthOffset' | Should -Be '1'
    }

    It 'passes through future invoice filter and sort values' {
        Get-Pax8Invoice -status 'FutureStatus' -sort 'futureField' -direction 'desc' -page 1 -size 10 | Out-Null

        $request = Get-CapturedRequest

        Get-QueryValue -Uri $request.Uri -Name 'status' | Should -Be 'FutureStatus'
        Get-QueryValue -Uri $request.Uri -Name 'sort' | Should -Be 'futureField,desc'
    }

    It 'passes through future product sort values' {
        Get-Pax8Product -sort 'futureSort' -direction 'asc' -page 1 -size 10 | Out-Null

        $request = Get-CapturedRequest

        Get-QueryValue -Uri $request.Uri -Name 'sort' | Should -Be 'futureSort,asc'
    }

    It 'passes through future subscription filter, sort, and update values' {
        Get-Pax8Subscription -billingTerm 'FutureTerm' -status 'FutureStatus' -sort 'futureSort' -direction 'desc' -page 1 -size 10 | Out-Null
        Update-Pax8SubscriptionById -subscriptionId 'subscription-1' -billingTerm 'FutureTerm' -Confirm:$false | Out-Null

        $requests = Get-CapturedRequest
        $subscriptionBody = $requests[1].Body | ConvertFrom-Json

        Get-QueryValue -Uri $requests[0].Uri -Name 'billingTerm' | Should -Be 'FutureTerm'
        Get-QueryValue -Uri $requests[0].Uri -Name 'status' | Should -Be 'FutureStatus'
        Get-QueryValue -Uri $requests[0].Uri -Name 'sort' | Should -Be 'futureSort,desc'
        $subscriptionBody.billingTerm | Should -Be 'FutureTerm'
    }

    It 'rejects unsupported sort directions' {
        { Get-Pax8Product -sort 'futureSort' -direction 'sideways' -page 1 -size 10 } | Should -Throw
    }

    It 'passes through future order values' {
        New-Pax8Order `
            -companyId ([guid]::NewGuid()) `
            -orderedBy 'FutureActor' `
            -orderedByUserEmail 'buyer@example.com' `
            -lineItems @(@{ productId = 'product-1'; lineItemNumber = 1; quantity = 1; billingTerm = 'FutureTerm' }) `
            -Confirm:$false | Out-Null

        $request = Get-CapturedRequest
        $orderBody = $request.Body | ConvertFrom-Json

        $orderBody.orderedBy | Should -Be 'FutureActor'
        $orderBody.lineItems[0].billingTerm | Should -Be 'FutureTerm'
    }

    It 'rejects unsupported draft invoice item month offsets' {
        { Get-Pax8InvoiceDraftItem -monthOffset 2 -page 1 -size 10 } | Should -Throw
    }
}

Describe 'PAX8 response shaping' {
    BeforeEach {
        InModuleScope 'Pax8-API' {
            $script:Pax8RestUrl = 'https://api.example.test/v1'
        }
    }

    It 'returns content items from paged responses' {
        Mock -ModuleName 'Pax8-API' -CommandName Invoke-Pax8MasterRequest -MockWith {
            [pscustomobject]@{
                content = @(
                    [pscustomobject]@{ id = 'product-1' }
                    [pscustomobject]@{ id = 'product-2' }
                )
                page    = [pscustomobject]@{
                    totalElements = 2
                }
            }
        }

        $result = Get-Pax8Product -page 0 -size 5

        @($result).Count | Should -Be 2
        $result[0].id | Should -Be 'product-1'
    }

    It 'returns no output from empty paged responses' {
        Mock -ModuleName 'Pax8-API' -CommandName Invoke-Pax8MasterRequest -MockWith {
            [pscustomobject]@{
                content = @()
                page    = [pscustomobject]@{
                    totalElements = 0
                    totalPages    = 0
                    size          = 5
                    number        = 0
                }
            }
        }

        $result = Get-Pax8InvoiceDraftItem -companyId ([guid]::NewGuid()) -monthOffset 1 -page 0 -size 5

        @($result).Count | Should -Be 0
    }

    It 'returns single-object responses without a content property' {
        $companyId = [guid]::NewGuid()

        Mock -ModuleName 'Pax8-API' -CommandName Invoke-Pax8MasterRequest -MockWith {
            [pscustomobject]@{
                id   = $companyId
                name = 'Initech'
            }
        }

        $result = Get-Pax8CompanyById -companyId $companyId

        $result.id | Should -Be $companyId
        $result.name | Should -Be 'Initech'
    }
}