< Summary

Information
Class: Elsa.Workflows.Api.Endpoints.Bookmarks.Resume.Resume
Assembly: Elsa.Workflows.Api
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs
Line coverage
5%
Covered lines: 5
Uncovered lines: 82
Coverable lines: 87
Total lines: 187
Line coverage: 5.7%
Branch coverage
0%
Covered branches: 0
Total branches: 34
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Configure()100%11100%
HandleAsync()0%7280%
GetInputFromQueryString()0%620%
GetInputAsync()0%620%
GetInputFromBodyAsync()0%7280%
ReadRequestBodyAsync()0%156120%
ResumeBookmarkedWorkflowAsync()0%620%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs

#LineLine coverage
 1using System.Text;
 2using System.Text.Json;
 3using Elsa.Abstractions;
 4using Elsa.SasTokens.Contracts;
 5using Elsa.Workflows;
 6using Elsa.Workflows.Runtime;
 7using FastEndpoints;
 8using JetBrains.Annotations;
 9using Microsoft.AspNetCore.Http;
 10
 11namespace Elsa.Workflows.Api.Endpoints.Bookmarks.Resume;
 12
 13/// <summary>
 14/// Resumes a bookmarked workflow instance with the bookmark ID specified in the provided SAS token.
 15/// </summary>
 16[PublicAPI]
 317internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResumer, IBookmarkQueue bookmarkQueue, IPaylo
 18{
 19    private const long MaxBookmarkResumeBodySize = 1024 * 1024;
 20
 21    /// <inheritdoc />
 22    public override void Configure()
 23    {
 324        Routes("/bookmarks/resume");
 325        Verbs(Http.GET, Http.POST);
 326        AllowAnonymous();
 327    }
 28
 29    /// <inheritdoc />
 30    public override async Task HandleAsync(CancellationToken cancellationToken)
 31    {
 032        var token = Query<string?>("t", false);
 33
 034        if (string.IsNullOrWhiteSpace(token) || !tokenService.TryDecryptToken<BookmarkTokenPayload>(token, out var paylo
 35        {
 036            AddError("Invalid token.");
 037            await Send.ErrorsAsync(cancellation: cancellationToken);
 038            return;
 39        }
 40
 041        var asynchronous = Query<bool>("async", false);
 042        var input = await GetInputAsync(cancellationToken);
 43
 044        if (ValidationFailed)
 45        {
 046            await Send.ErrorsAsync(cancellation: cancellationToken);
 047            return;
 48        }
 49
 50        // Some clients, like Blazor, may prematurely cancel their request upon navigation away from the page.
 51        // In this case, we don't want to cancel the workflow execution.
 52        // We need to better understand the conditions that cause this.
 053        var workflowCancellationToken = CancellationToken.None;
 054        await ResumeBookmarkedWorkflowAsync(payload, input, asynchronous, workflowCancellationToken);
 55
 056        if (!HttpContext.Response.HasStarted)
 057            await Send.OkAsync(cancellation: cancellationToken);
 058    }
 59
 60    private IDictionary<string, object>? GetInputFromQueryString()
 61    {
 062        var inputJson = Query<string?>("in", false);
 063        if (string.IsNullOrWhiteSpace(inputJson))
 064            return null;
 65
 66        try
 67        {
 068            return payloadSerializer.Deserialize<IDictionary<string, object>>(inputJson);
 69        }
 070        catch (Exception e) when (e is JsonException or NotSupportedException or InvalidOperationException or FormatExce
 71        {
 072            AddError("Invalid input format. Expected a valid JSON string.");
 073            return null;
 74        }
 075    }
 76
 77    private async ValueTask<IDictionary<string, object>?> GetInputAsync(CancellationToken cancellationToken)
 78    {
 079        return HttpContext.Request.Method == HttpMethods.Post
 080            ? await GetInputFromBodyAsync(cancellationToken)
 081            : GetInputFromQueryString();
 082    }
 83
 84    private async ValueTask<IDictionary<string, object>?> GetInputFromBodyAsync(CancellationToken cancellationToken)
 85    {
 086        if (HttpContext.Request.ContentLength == 0)
 087            return null;
 88
 089        var body = await ReadRequestBodyAsync(cancellationToken);
 090        if (ValidationFailed)
 091            return null;
 92
 093        if (string.IsNullOrWhiteSpace(body))
 094            return null;
 95
 96        try
 97        {
 098            var request = apiSerializer.Deserialize<Request>(body);
 099            if (request == null)
 100            {
 0101                AddError("Invalid input format. Expected a valid JSON request body.");
 0102                return null;
 103            }
 104
 0105            return request.Input;
 106        }
 0107        catch (Exception e) when (e is JsonException or NotSupportedException or InvalidOperationException or FormatExce
 108        {
 0109            AddError("Invalid input format. Expected a valid JSON request body.");
 0110            return null;
 111        }
 0112    }
 113
 114    private async ValueTask<string?> ReadRequestBodyAsync(CancellationToken cancellationToken)
 115    {
 0116        var request = HttpContext.Request;
 0117        if (request.ContentLength is > MaxBookmarkResumeBodySize)
 118        {
 0119            AddError("Request body is too large.");
 0120            return null;
 121        }
 122
 0123        await using var body = new MemoryStream();
 0124        var buffer = new byte[81920];
 0125        long totalBytesRead = 0;
 126
 0127        while (true)
 128        {
 0129            var bytesRead = await request.Body.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
 0130            if (bytesRead == 0)
 131                break;
 132
 0133            totalBytesRead += bytesRead;
 0134            if (totalBytesRead > MaxBookmarkResumeBodySize)
 135            {
 0136                AddError("Request body is too large.");
 0137                return null;
 138            }
 139
 0140            await body.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
 141        }
 142
 0143        return body.Length == 0 ? null : Encoding.UTF8.GetString(body.ToArray());
 0144    }
 145
 146    private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary<string, object>? inp
 147    {
 0148        var bookmarkId = tokenPayload.BookmarkId;
 0149        var workflowInstanceId = tokenPayload.WorkflowInstanceId;
 150
 0151        if (asynchronous)
 152        {
 0153            var item = new NewBookmarkQueueItem
 0154            {
 0155                BookmarkId = bookmarkId,
 0156                WorkflowInstanceId = workflowInstanceId,
 0157                Options = new()
 0158                {
 0159                    Input = input
 0160                }
 0161            };
 162
 0163            await bookmarkQueue.EnqueueAsync(item, cancellationToken);
 0164            return;
 165        }
 166
 0167        var resumeRequest = new ResumeBookmarkRequest
 0168        {
 0169            BookmarkId = bookmarkId,
 0170            WorkflowInstanceId = workflowInstanceId,
 0171            Input = input
 0172        };
 173
 0174        await workflowResumer.ResumeAsync(resumeRequest, cancellationToken);
 0175    }
 176}
 177
 178/// <summary>
 179/// The request model for the Resume endpoint.
 180/// </summary>
 181internal class Request
 182{
 183    /// <summary>
 184    /// The input to provide to the workflow when resuming.
 185    /// </summary>
 186    public IDictionary<string, object>? Input { get; set; }
 187}