Modules/businessdev.ALbuild.Apps/Private/Invoke-BcNativeContainerTest.ps1

function Invoke-BcNativeContainerTest {
    <#
    .SYNOPSIS
        Runs ALbuild's built-in AL test runner inside a container.
 
    .DESCRIPTION
        Internal helper behind Invoke-BcContainerTest. Resolves the credentials and the set of test
        apps (from a project folder, honouring pipeline.config, or from explicit extension ids),
        copies the in-container runner payload into the container, and invokes it to produce the
        JUnit result file at -ResultPathInContainer. The caller copies the file out and parses it.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The containerPassword pipeline variable arrives as plain text and must be turned into a PSCredential to open the in-container client session.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Name,
        [Parameter(Mandatory)] [string] $ResultPathInContainer,
        [string] $ProjectFolder,
        [string[]] $ExtensionId,
        [pscredential] $Credential,
        [string] $TestSuite = 'DEFAULT',
        [int] $TestRunnerCodeunitId,
        [switch] $DisableIsolation,
        [string] $Tenant = 'default',
        [string] $CompanyName = '',
        [string] $Culture = 'en-US',
        [string] $Auth = 'NavUserPassword',
        [switch] $AzureDevOps,
        [string] $DockerExecutable = 'docker'
    )

    # --- Resolve credentials --------------------------------------------------------------------
    if (-not $Credential -and $Auth -ne 'Windows') {
        if ($env:containerUsername -and $env:containerPassword) {
            $secure = ConvertTo-SecureString -String $env:containerPassword -AsPlainText -Force
            $Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $env:containerUsername, $secure
        }
        else {
            throw "Credentials are required for '$Auth' authentication. Pass -Credential or set the containerUsername/containerPassword environment variables."
        }
    }

    $userName = ''
    $password = ''
    if ($Credential) {
        $userName = $Credential.UserName
        $password = $Credential.GetNetworkCredential().Password
    }

    # --- Build the list of test apps to run ----------------------------------------------------
    # Per-app runner codeunit and suite come from the project config (albuild.json); the cmdlet
    # parameters act as global overrides: -TestRunnerCodeunitId forces an id for every app, and
    # -DisableIsolation switches the default runner for apps that do not pin one.
    $testApps = [System.Collections.Generic.List[object]]::new()
    if ($ExtensionId) {
        $runnerId = if ($PSBoundParameters.ContainsKey('TestRunnerCodeunitId')) { $TestRunnerCodeunitId }
        elseif ($DisableIsolation) { 130451 } else { 130450 }
        foreach ($id in $ExtensionId) {
            $testApps.Add([PSCustomObject]@{ ExtensionId = $id; AppName = ''; TestRunnerCodeunitId = $runnerId; TestSuite = $TestSuite })
        }
    }
    else {
        if (-not $ProjectFolder) { $ProjectFolder = (Get-Location).Path }
        $discovered = @(Get-BcTestAppFolder -ProjectFolder $ProjectFolder)
        if ($discovered.Count -eq 0) {
            throw "No AL test apps were found under '$ProjectFolder'."
        }
        foreach ($app in $discovered) {
            $cfg = $app.ProjectConfig
            if ($PSBoundParameters.ContainsKey('TestRunnerCodeunitId')) { $runnerId = $TestRunnerCodeunitId }
            elseif ($cfg.TestRunnerCodeunitId -gt 0) { $runnerId = $cfg.TestRunnerCodeunitId }
            elseif ($DisableIsolation) { $runnerId = 130451 }
            else { $runnerId = $cfg.EffectiveTestRunnerCodeunitId() }
            $suite = if ($PSBoundParameters.ContainsKey('TestSuite')) { $TestSuite } else { $cfg.TestSuite }
            Write-ALbuildLog "Test app '$($app.Name)' ($($app.Id)) -> suite '$suite', runner codeunit $runnerId"
            $testApps.Add([PSCustomObject]@{ ExtensionId = $app.Id; AppName = $app.Name; TestRunnerCodeunitId = $runnerId; TestSuite = $suite })
        }
    }

    # --- Stage the in-container runner payload --------------------------------------------------
    $resourceFolder = Join-Path -Path $script:ModuleRoot -ChildPath 'Resources/TestRunner'
    $clientContextScript = Join-Path -Path $resourceFolder -ChildPath 'BcTestClientContext.ps1'
    $runnerScript = Join-Path -Path $resourceFolder -ChildPath 'Invoke-BcAlTestRun.ps1'
    foreach ($payload in @($clientContextScript, $runnerScript)) {
        if (-not (Test-Path -LiteralPath $payload)) { throw "Test runner payload '$payload' is missing from the module." }
    }

    $resultDirInContainer = Split-Path -Path $ResultPathInContainer -Parent

    Invoke-BcContainerCommand -ContainerName $Name -DockerExecutable $DockerExecutable `
        -Variables @{ ResultDir = $resultDirInContainer } -ScriptBlock {
        New-Item -ItemType Directory -Path $ResultDir -Force | Out-Null
    } | Out-Null

    # Both runner scripts are shared into C:\run\my (same folder), so the runner finds its sibling.
    [void](Copy-BcFileToContainer -Name $Name -DockerExecutable $DockerExecutable -Source $clientContextScript)
    $runnerInContainer = Copy-BcFileToContainer -Name $Name -DockerExecutable $DockerExecutable -Source $runnerScript
    $containerRunnerDir = Split-Path -Path $runnerInContainer -Parent

    # --- Run the tests in the container ---------------------------------------------------------
    $azureDevOpsMode = if ($AzureDevOps) { 'warning' } else { 'no' }

    $output = Invoke-BcContainerCommand -ContainerName $Name -DockerExecutable $DockerExecutable -StreamOutput -Variables @{
        RunnerDir   = $containerRunnerDir
        TestApps    = @($testApps)
        TestSuite   = $TestSuite
        JUnitPath   = $ResultPathInContainer
        Tenant      = $Tenant
        CompanyName = $CompanyName
        Auth        = $Auth
        RunnerUser  = $userName
        RunnerPass  = $password
        Culture     = $Culture
        AzureDevOps = $azureDevOpsMode
    } -ScriptBlock {
        $runner = Join-Path $RunnerDir 'Invoke-BcAlTestRun.ps1'
        & $runner -TestApps @($TestApps) -TestSuite $TestSuite -JUnitResultFileName $JUnitPath `
            -Tenant $Tenant -CompanyName $CompanyName -Auth $Auth -UserName $RunnerUser -Password $RunnerPass `
            -Culture $Culture -AzureDevOps $AzureDevOps
    }

    $null = $output   # the runner output already streamed live via -StreamOutput
}