< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.PowerShellDelegateBuilder
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
67%
Covered lines: 59
Uncovered lines: 29
Coverable lines: 88
Total lines: 250
Line coverage: 67%
Branch coverage
53%
Covered branches: 32
Total branches: 60
Branch coverage: 53.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/08/2025 - 20:34:03 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7210/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/14/2025 - 12:29:34 Line coverage: 87.6% (92/105) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 87.7% (93/106) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.5% (103/119) Branch coverage: 70% (21/30) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd01/12/2026 - 18:03:06 Line coverage: 85.4% (112/131) Branch coverage: 70% (21/30) Total lines: 220 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/17/2026 - 04:33:35 Line coverage: 80.4% (115/143) Branch coverage: 70% (21/30) Total lines: 232 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 67.4% (58/86) Branch coverage: 53.4% (31/58) Total lines: 246 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/24/2026 - 19:35:59 Line coverage: 67% (59/88) Branch coverage: 53.3% (32/60) Total lines: 250 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa51 09/08/2025 - 20:34:03 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7210/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/14/2025 - 12:29:34 Line coverage: 87.6% (92/105) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 87.7% (93/106) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.5% (103/119) Branch coverage: 70% (21/30) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd01/12/2026 - 18:03:06 Line coverage: 85.4% (112/131) Branch coverage: 70% (21/30) Total lines: 220 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/17/2026 - 04:33:35 Line coverage: 80.4% (115/143) Branch coverage: 70% (21/30) Total lines: 232 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 67.4% (58/86) Branch coverage: 53.4% (31/58) Total lines: 246 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/24/2026 - 19:35:59 Line coverage: 67% (59/88) Branch coverage: 53.3% (32/60) Total lines: 250 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa51

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)100%22100%
ExecutePowerShellRequestAsync()36.66%1503048.97%
GetPowerShellFromContext(...)66.66%66100%
GetKestrunContext(...)50%22100%
LogTopResults(...)66.66%7675%
HandleErrorsIfAnyAsync()100%44100%
LogSideChannelMessagesIfAny(...)50%12860%
HandleRedirectIfAny(...)100%22100%
ApplyResponseAsync(...)100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs

#LineLine coverage
 1using System.Management.Automation;
 2using Kestrun.Hosting;
 3using Kestrun.Logging;
 4using Kestrun.Models;
 5using Kestrun.Utilities;
 6using Serilog.Events;
 7
 8namespace Kestrun.Languages;
 9
 10internal static class PowerShellDelegateBuilder
 11{
 12    public const string PS_INSTANCE_KEY = "PS_INSTANCE";
 13    public const string KR_CONTEXT_KEY = "KR_CONTEXT";
 14    internal static RequestDelegate Build(KestrunHost host, string code, Dictionary<string, object?>? arguments)
 15    {
 616        var log = host.Logger;
 617        ArgumentNullException.ThrowIfNull(code);
 618        if (log.IsEnabled(LogEventLevel.Debug))
 19        {
 420            log.Debug("Building PowerShell delegate, script length={Length}", code.Length);
 21        }
 22
 1123        return context => ExecutePowerShellRequestAsync(context, log, code, arguments);
 24    }
 25
 26    /// <summary>
 27    /// Executes the PowerShell request pipeline and applies the resulting response.
 28    /// </summary>
 29    /// <param name="context">Current HTTP context.</param>
 30    /// <param name="log">Logger instance.</param>
 31    /// <param name="code">PowerShell script code.</param>
 32    /// <param name="arguments">Arguments to inject as variables into the script.</param>
 33    private static async Task ExecutePowerShellRequestAsync(
 34        HttpContext context,
 35        Serilog.ILogger log,
 36        string code,
 37        Dictionary<string, object?>? arguments)
 38    {
 539        var isLogVerbose = log.IsEnabled(LogEventLevel.Verbose);
 40        // Log invocation
 541        if (log.IsEnabled(LogEventLevel.Debug))
 42        {
 343            log.DebugSanitized("PS delegate invoked for {Path}", context.Request.Path);
 44        }
 45
 46        // Prepare for execution
 547        KestrunContext? krContext = null;
 48        // Get the PowerShell instance from the context (set by middleware)
 549        var ps = GetPowerShellFromContext(context, log);
 50
 51        // Ensure the runspace pool is open before executing the script
 52        try
 53        {
 454            PowerShellExecutionHelpers.SetVariables(ps, arguments, log);
 455            if (isLogVerbose)
 56            {
 057                log.Verbose("Setting PowerShell variables for Request and Response in the runspace.");
 58            }
 459            krContext = GetKestrunContext(context);
 60
 461            if (krContext.HasRequestCulture)
 62            {
 063                PowerShellExecutionHelpers.AddCulturePrelude(ps, krContext.Culture, log);
 64            }
 465            PowerShellExecutionHelpers.AddScript(ps, code);
 66
 67            // Extract and add parameters for injection
 468            ParameterForInjectionInfo.InjectParameters(krContext, ps);
 69
 70            // Execute the script
 471            if (isLogVerbose)
 72            {
 073                log.Verbose("Invoking PowerShell script...");
 74            }
 475            var psResults = await ps.InvokeAsync(log, context.RequestAborted).ConfigureAwait(false);
 476            LogTopResults(log, psResults);
 77
 478            if (await HandleErrorsIfAnyAsync(context, ps).ConfigureAwait(false))
 79            {
 180                return;
 81            }
 82
 383            LogSideChannelMessagesIfAny(log, ps);
 84
 385            if (HandleRedirectIfAny(context, krContext, log))
 86            {
 187                return;
 88            }
 89
 90            // Some endpoints (e.g., SSE streaming) write directly to the HttpResponse and
 91            // intentionally start the response early. In that case, applying KestrunResponse
 92            // would attempt to set headers/status again and throw.
 293            if (context.Response.HasStarted)
 94            {
 095                if (isLogVerbose)
 96                {
 097                    log.Verbose("HttpResponse has already started; skipping KestrunResponse.ApplyTo().");
 98                }
 099                return;
 100            }
 2101            if (isLogVerbose)
 102            {
 0103                log.Verbose("No redirect detected; applying response to HttpResponse...");
 104            }
 2105            await ApplyResponseAsync(context, krContext).ConfigureAwait(false);
 2106        }
 107        // optional: catch client cancellation to avoid noisy logs
 0108        catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
 109        {
 110            // client disconnected – nothing to send
 0111        }
 0112        catch (ParameterBindingException pbaex)
 113        {
 0114            var fqid = pbaex.ErrorRecord?.FullyQualifiedErrorId;
 0115            var cat = pbaex.ErrorRecord?.CategoryInfo?.Category;
 116            // Log parameter binding errors with preview of code
 0117            log.Error("PowerShell parameter binding error ({Category}/{FQID}) - {Preview}",
 0118                cat, fqid, code[..Math.Min(40, code.Length)]);
 119            // Return 400 Bad Request for parameter binding errors
 0120            context.Response.StatusCode = StatusCodes.Status400BadRequest;
 0121            context.Response.ContentType = "text/plain; charset=utf-8";
 0122            await context.Response.WriteAsync("Invalid request parameters.");
 0123        }
 0124        catch (Exception ex)
 125        {
 126            // If we have exception options, set a 500 status code and generic message.
 127            // Otherwise rethrow to let higher-level middleware handle it (e.g., Developer Exception Page
 0128            if (krContext?.Host?.ExceptionOptions is null)
 129            { // Log and handle script errors
 0130                log.Error(ex, "PowerShell script failed - {Preview}", code[..Math.Min(40, code.Length)]);
 0131                context.Response.StatusCode = 500; // Internal Server Error
 0132                context.Response.ContentType = "text/plain; charset=utf-8";
 0133                await context.Response.WriteAsync("An error occurred while processing your request.");
 134            }
 135            else
 136            {
 137                // re-throw to let higher-level middleware handle it (e.g., Developer Exception Page)
 0138                throw;
 139            }
 140        }
 141        finally
 142        {
 143            // Do not call Response.CompleteAsync here; leaving the response open allows
 144            // downstream middleware like StatusCodePages to generate a body for status-only responses.
 145        }
 4146    }
 147
 148    /// <summary>
 149    /// Retrieves the PowerShell instance from the HttpContext items.
 150    /// </summary>
 151    /// <param name="context">The HttpContext from which to retrieve the PowerShell instance.</param>
 152    /// <param name="log">The logger to use for logging.</param>
 153    /// <returns>The PowerShell instance associated with the current request.</returns>
 154    /// <exception cref="InvalidOperationException">Thrown if the PowerShell instance is not found in the context items.
 155    private static PowerShell GetPowerShellFromContext(HttpContext context, Serilog.ILogger log)
 156    {
 5157        if (!context.Items.ContainsKey(PS_INSTANCE_KEY))
 158        {
 1159            throw new InvalidOperationException("PowerShell runspace not found in context items. Ensure PowerShellRunspa
 160        }
 161
 4162        log.Verbose("Retrieving PowerShell instance from context items.");
 4163        var ps = context.Items[PS_INSTANCE_KEY] as PowerShell
 4164                 ?? throw new InvalidOperationException("PowerShell instance not found in context items.");
 4165        return ps.Runspace == null
 4166            ? throw new InvalidOperationException("PowerShell runspace is not set. Ensure PowerShellRunspaceMiddleware i
 4167            : ps;
 168    }
 169
 170    /// <summary>
 171    /// Retrieves the KestrunContext from the HttpContext items.
 172    /// </summary>
 173    /// <param name="context">The HttpContext from which to retrieve the KestrunContext.</param>
 174    /// <returns>The KestrunContext associated with the current request.</returns>
 175    /// <exception cref="InvalidOperationException">Thrown if the KestrunContext is not found in the context items.</exc
 176    private static KestrunContext GetKestrunContext(HttpContext context)
 4177        => context.Items[KR_CONTEXT_KEY] as KestrunContext
 4178           ?? throw new InvalidOperationException($"{KR_CONTEXT_KEY} key not found in context items.");
 179
 180    ///<summary>
 181    /// Logs the top results from the PowerShell script output for debugging purposes.
 182    /// Only logs if the log level is set to Debug.
 183    /// </summary>
 184    /// <param name="log">The logger to use for logging.</param>
 185    /// <param name="psResults">The collection of PSObject results from the PowerShell script.</param>
 186    private static void LogTopResults(Serilog.ILogger log, PSDataCollection<PSObject> psResults)
 187    {
 4188        if (!log.IsEnabled(LogEventLevel.Debug))
 189        {
 2190            return;
 191        }
 192
 2193        log.Debug("PowerShell script output:");
 4194        foreach (var r in psResults.Take(10))
 195        {
 0196            log.Debug("   • {Result}", r);
 197        }
 2198        if (psResults.Count > 10)
 199        {
 0200            log.Debug("   … {Count} more", psResults.Count - 10);
 201        }
 2202    }
 203
 204    /// <summary>
 205    /// Handles any errors that occurred during the PowerShell script execution.
 206    /// </summary>
 207    /// <param name="context">The HttpContext for the current request.</param>
 208    /// <param name="ps">The PowerShell instance used for script execution.</param>
 209    /// <returns>True if errors were handled, false otherwise.</returns>
 210    private static async Task<bool> HandleErrorsIfAnyAsync(HttpContext context, PowerShell ps)
 211    {
 4212        if (ps.HadErrors || ps.Streams.Error.Count != 0)
 213        {
 1214            await BuildError.ResponseAsync(context, ps).ConfigureAwait(false);
 1215            return true;
 216        }
 3217        return false;
 4218    }
 219
 220    /// <summary>
 221    /// Logs any side-channel messages (Verbose, Debug, Warning, Information) produced by the PowerShell script.
 222    /// </summary>
 223    /// <param name="log">The logger to use for logging.</param>
 224    /// <param name="ps">The PowerShell instance used to invoke the script.</param>
 225    private static void LogSideChannelMessagesIfAny(Serilog.ILogger log, PowerShell ps)
 226    {
 3227        if (ps.Streams.Verbose.Count > 0 || ps.Streams.Debug.Count > 0 || ps.Streams.Warning.Count > 0 || ps.Streams.Inf
 228        {
 0229            log.Verbose("PowerShell script completed with verbose/debug/warning/info messages.");
 0230            log.Verbose(BuildError.Text(ps));
 231        }
 3232        log.Verbose("PowerShell script completed successfully.");
 3233    }
 234
 235    private static bool HandleRedirectIfAny(HttpContext context, KestrunContext krContext, Serilog.ILogger log)
 236    {
 3237        if (!string.IsNullOrEmpty(krContext.Response.RedirectUrl))
 238        {
 1239            log.Verbose($"Redirecting to {krContext.Response.RedirectUrl}");
 1240            context.Response.Redirect(krContext.Response.RedirectUrl);
 1241            return true;
 242        }
 2243        return false;
 244    }
 245
 246    private static Task ApplyResponseAsync(HttpContext context, KestrunContext krContext)
 2247        => krContext.Response.ApplyTo(context.Response);
 248
 249    // Removed explicit Response.CompleteAsync to allow StatusCodePages to run after endpoints when appropriate.
 250}