Tests/Unit/Sharing.Tests.ps1

#Requires -Modules @{ModuleName='Pester';ModuleVersion='5.0.0'}

BeforeAll {
    $ModulePath = Join-Path (Join-Path $PSScriptRoot '..') (Join-Path '..' 'SubtitleTools.psd1')
    Import-Module $ModulePath -Force
}

Describe 'ConvertTo-MultipartFormBody' {
    # Invoke-RestMethod -Form is PowerShell 6.1+, so Publish-SubtitleFile threw a
    # parameter binding error on Windows PowerShell 5.1 - the edition the manifest
    # claims to support. The body is now built here, and these tests are the only
    # thing standing behind it: SubDL is a third-party API with no test endpoint, so
    # correctness is established against the wire format, not against their server.

    BeforeAll {
        # Latin-1 maps bytes 1:1 onto chars, so string offsets into the decoded body
        # are byte offsets - which is what makes it possible to slice the file part
        # back out and compare it byte for byte.
        $Latin1 = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
    }

    It 'Emits a boundary that matches the ContentType and closes the body with it' {
        $result = InModuleScope SubtitleTools {
            ConvertTo-MultipartFormBody -Fields ([ordered]@{ a = '1' })
        }

        $result.ContentType | Should -Match '^multipart/form-data; boundary=[0-9a-f]{32}$'

        $boundary = $result.ContentType -replace '.*boundary=', ''
        $text     = [System.Text.Encoding]::UTF8.GetString($result.Body)

        $text | Should -BeLike "--$boundary`r`n*"
        $text | Should -BeLike "*`r`n--$boundary--`r`n"
    }

    It 'Writes each text field as its own part, in the order given' {
        $result = InModuleScope SubtitleTools {
            ConvertTo-MultipartFormBody -Fields ([ordered]@{
                n_id = 'session-123'
                type = 'tv'
                lang = 'Farsi/Persian'
            })
        }

        $text = [System.Text.Encoding]::UTF8.GetString($result.Body)

        $text | Should -Match 'Content-Disposition: form-data; name="n_id"\r\n\r\nsession-123\r\n'
        $text | Should -Match 'Content-Disposition: form-data; name="type"\r\n\r\ntv\r\n'
        $text | Should -Match 'Content-Disposition: form-data; name="lang"\r\n\r\nFarsi/Persian\r\n'

        # [ordered] in, same order out.
        $text.IndexOf('name="n_id"') | Should -BeLessThan $text.IndexOf('name="type"')
        $text.IndexOf('name="type"') | Should -BeLessThan $text.IndexOf('name="lang"')
    }

    It 'Carries a non-ASCII field value through as UTF-8' {
        $result = InModuleScope SubtitleTools {
            ConvertTo-MultipartFormBody -Fields ([ordered]@{ name = 'ژاپنی' })
        }

        # Decoded as UTF-8 it must read back identically, which fails if the encoder
        # is ever swapped for ASCII or the console code page - both of which turn
        # these characters into '?' rather than erroring.
        $text = [System.Text.Encoding]::UTF8.GetString($result.Body)
        $text | Should -Match ([regex]::Escape('name="name"') + '\r\n\r\n' + [regex]::Escape('ژاپنی') + '\r\n')

        # 5 Persian characters, 2 UTF-8 bytes each - not 5 bytes of '?'.
        $valueBytes = [System.Text.Encoding]::UTF8.GetBytes('ژاپنی')
        $valueBytes.Count | Should -Be 10
        $Latin1.GetString($result.Body) | Should -BeLike "*$($Latin1.GetString($valueBytes))*"
    }

    It 'Copies the attached file through byte for byte, including bytes no string survives' {
        $filePath = Join-Path $TestDrive 'subtitle.srt'

        # Deliberately hostile content: Persian text (multi-byte UTF-8), a UTF-8 BOM,
        # a lone NUL, and a bare LF that must not be normalised to CRLF.
        $original = [byte[]] (
            @(0xEF, 0xBB, 0xBF) +
            [System.Text.Encoding]::UTF8.GetBytes("1`n00:00:01,000 --> 00:00:02,000`nسلام دنیا`n") +
            @(0x00, 0xFF, 0x0A)
        )
        [System.IO.File]::WriteAllBytes($filePath, $original)

        $result = InModuleScope SubtitleTools -Parameters @{ filePath = $filePath } {
            param($filePath)
            ConvertTo-MultipartFormBody `
                -Fields ([ordered]@{ n_id = 'abc' }) `
                -FilePath $filePath -FileFieldName 'subtitle'
        }

        $boundary = $result.ContentType -replace '.*boundary=', ''
        $decoded  = $Latin1.GetString($result.Body)

        $decoded | Should -Match 'Content-Disposition: form-data; name="subtitle"; filename="subtitle\.srt"'
        $decoded | Should -Match 'Content-Type: application/octet-stream'

        $marker = "Content-Type: application/octet-stream`r`n`r`n"
        $start  = $decoded.IndexOf($marker) + $marker.Length
        $end    = $decoded.LastIndexOf("`r`n--$boundary--")
        $start  | Should -BeGreaterThan 0
        $end    | Should -BeGreaterThan $start

        $sent = $result.Body[$start..($end - 1)]
        $sent.Count | Should -Be $original.Count
        # -Be on two byte arrays compares element-wise, so a single flipped byte fails.
        $sent | Should -Be $original
    }

    It 'Keeps a non-ASCII filename in the part header instead of mangling it' {
        # A Persian .srt is exactly what this module produces, so the filename is
        # routinely non-ASCII. RFC 7578 says send it as UTF-8; that is what browsers
        # do and what SubDL will be expecting.
        $filePath = Join-Path $TestDrive 'زیرنویس.srt'
        [System.IO.File]::WriteAllBytes($filePath, [byte[]] @(0x41))

        $result = InModuleScope SubtitleTools -Parameters @{ filePath = $filePath } {
            param($filePath)
            ConvertTo-MultipartFormBody -FilePath $filePath -FileFieldName 'subtitle'
        }

        $text = [System.Text.Encoding]::UTF8.GetString($result.Body)
        $text | Should -Match ([regex]::Escape('filename="زیرنویس.srt"'))
    }

    It 'Refuses a file with no field name rather than silently dropping the part' {
        {
            InModuleScope SubtitleTools {
                ConvertTo-MultipartFormBody -FilePath 'anything.srt'
            }
        } | Should -Throw -ExpectedMessage '*FileFieldName is required*'
    }
}

Describe 'Invoke-SubDLUpload PowerShell 5.1 compatibility' {
    It 'Never passes -Form, which does not exist before PowerShell 6.1' {
        # The regression guard for the actual bug: -Form binds fine on the sandbox's
        # PowerShell 7 and throws on the 5.1 host this module claims to support, so
        # no runtime test on this box can catch its return. Read the source instead.
        $source = Get-Content (Join-Path $PSScriptRoot '../../Private/Sharing/Invoke-SubDLUpload.ps1') -Raw
        $source | Should -Not -Match '(?m)^\s*-Form\b'
        $source | Should -Match 'ConvertTo-MultipartFormBody'
    }

    It 'Sends the multipart body and its matching content type together on every upload step' {
        # Body and ContentType carry the same boundary; pairing a body with someone
        # else's content type parses server-side as an empty form, which SubDL would
        # report as a vague failure rather than an error.
        $source = Get-Content (Join-Path $PSScriptRoot '../../Private/Sharing/Invoke-SubDLUpload.ps1') -Raw

        $contentTypes = ([regex]::Matches($source, '-ContentType \$(\w+)\.ContentType')  | ForEach-Object { $_.Groups[1].Value })
        $bodies       = ([regex]::Matches($source, '-Body \$(\w+)\.Body')                | ForEach-Object { $_.Groups[1].Value })

        @($contentTypes).Count | Should -Be 2
        @($bodies).Count       | Should -Be 2
        $contentTypes          | Should -Be $bodies
    }
}