Honor label filters when listing workflow definitions (#8035)
* Honor label filters when listing workflow definitions * Authorize label lookups and require filter resolution
This commit is contained in:
parent
5f692b956d
commit
cbdc3f7e9d
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Labels.Entities;
|
||||
|
||||
namespace Elsa.Labels.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Queries workflow definition label relationships for workflow-definition filter providers.
|
||||
/// </summary>
|
||||
public interface IWorkflowDefinitionLabelQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns workflow definition versions associated with any of the specified label IDs. Matching is based on the workflow definition version ID; an empty or unknown set returns no associations.
|
||||
/// </summary>
|
||||
Task<IEnumerable<WorkflowDefinitionLabel>> FindByLabelIdsAsync(IEnumerable<string> labelIds, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using Elsa.Features.Services;
|
|||
using Elsa.Labels.Contracts;
|
||||
using Elsa.Labels.Entities;
|
||||
using Elsa.Labels.Services;
|
||||
using Elsa.Workflows.Management;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Labels.Features;
|
||||
|
|
@ -45,6 +46,7 @@ public class LabelsFeature : FeatureBase
|
|||
.AddMemoryStore<WorkflowDefinitionLabel, InMemoryWorkflowDefinitionLabelStore>()
|
||||
.AddScoped(LabelStore)
|
||||
.AddScoped(WorkflowDefinitionLabelStore)
|
||||
.AddScoped<IWorkflowDefinitionFilterProvider, WorkflowDefinitionLabelFilterProvider>()
|
||||
;
|
||||
|
||||
Services.AddNotificationHandlersFrom(GetType());
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace Elsa.Labels.Services;
|
|||
/// <summary>
|
||||
/// An in-memory store of workflow-label associations.
|
||||
/// </summary>
|
||||
public class InMemoryWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStore
|
||||
public class InMemoryWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStore, IWorkflowDefinitionLabelQuery
|
||||
{
|
||||
private readonly MemoryStore<WorkflowDefinitionLabel> _store;
|
||||
|
||||
|
|
@ -47,6 +47,14 @@ public class InMemoryWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStor
|
|||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<WorkflowDefinitionLabel>> FindByLabelIdsAsync(IEnumerable<string> labelIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ids = labelIds.ToHashSet();
|
||||
var result = _store.FindMany(x => ids.Contains(x.LabelId));
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ReplaceAsync(IEnumerable<WorkflowDefinitionLabel> removed, IEnumerable<WorkflowDefinitionLabel> added, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
using Elsa.Authorization;
|
||||
using Elsa.Labels.Contracts;
|
||||
using Elsa.Labels.Permissions;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Exceptions;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
|
||||
namespace Elsa.Labels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Applies label relationships to workflow definition filters. A version qualifies when it has any requested label ID; an unknown label produces no matches. A caller filtering by labels must hold the workflow-definition-label view permission. The configured label store must implement <see cref="IWorkflowDefinitionLabelQuery"/> to support this filter.
|
||||
/// </summary>
|
||||
public class WorkflowDefinitionLabelFilterProvider(IWorkflowDefinitionLabelStore store) : IWorkflowDefinitionFilterProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool CanApply(WorkflowDefinitionFilter filter) => filter.LabelIds is { Count: > 0 };
|
||||
|
||||
/// <summary>Requires workflow-definition-label view permission when a label filter is present.</summary>
|
||||
public IEnumerable<Permission> GetRequiredPermissions(WorkflowDefinitionFilter filter) =>
|
||||
CanApply(filter) ? [new(LabelPermissions.WorkflowDefinitionLabels, CoreVerbs.View)] : [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ApplyAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (filter.LabelIds is not { Count: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (store is not IWorkflowDefinitionLabelQuery query)
|
||||
{
|
||||
throw new WorkflowDefinitionFilterNotSupportedException("Filtering workflow definitions by labels is not supported by the configured label store.");
|
||||
}
|
||||
|
||||
var labelIds = filter.LabelIds.Distinct().ToList();
|
||||
var matchingVersionIds = (await query.FindByLabelIdsAsync(labelIds, cancellationToken))
|
||||
.Select(x => x.WorkflowDefinitionVersionId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
filter.Ids = filter.Ids == null
|
||||
? matchingVersionIds
|
||||
: filter.Ids.Intersect(matchingVersionIds).ToList();
|
||||
filter.LabelIds = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using Elsa.Labels.Contracts;
|
|||
using Elsa.Labels.Entities;
|
||||
using Elsa.Labels.Services;
|
||||
using Elsa.Platform.PackageManifest.Generator.Hints;
|
||||
using Elsa.Workflows.Management;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
|
@ -37,7 +38,8 @@ public class LabelsFeature : IShellFeature
|
|||
.AddMemoryStore<Label, InMemoryLabelStore>()
|
||||
.AddMemoryStore<WorkflowDefinitionLabel, InMemoryWorkflowDefinitionLabelStore>()
|
||||
.AddScoped(LabelStore)
|
||||
.AddScoped(WorkflowDefinitionLabelStore);
|
||||
.AddScoped(WorkflowDefinitionLabelStore)
|
||||
.AddScoped<IWorkflowDefinitionFilterProvider, WorkflowDefinitionLabelFilterProvider>();
|
||||
|
||||
services.AddNotificationHandlersFrom(GetType());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using Elsa.Labels.Entities;
|
|||
namespace Elsa.Persistence.EFCore.Modules.Labels;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class EFCoreWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStore
|
||||
public class EFCoreWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStore, IWorkflowDefinitionLabelQuery
|
||||
{
|
||||
private readonly EntityStore<LabelsElsaDbContext, WorkflowDefinitionLabel> _store;
|
||||
|
||||
|
|
@ -26,6 +26,13 @@ public class EFCoreWorkflowDefinitionLabelStore : IWorkflowDefinitionLabelStore
|
|||
public async Task<IEnumerable<WorkflowDefinitionLabel>> FindByWorkflowDefinitionVersionIdAsync(string workflowDefinitionVersionId, CancellationToken cancellationToken = default) =>
|
||||
await _store.FindManyAsync(x => x.WorkflowDefinitionVersionId == workflowDefinitionVersionId, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<WorkflowDefinitionLabel>> FindByLabelIdsAsync(IEnumerable<string> labelIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ids = labelIds.ToList();
|
||||
return await _store.FindManyAsync(x => ids.Contains(x.LabelId), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReplaceAsync(IEnumerable<WorkflowDefinitionLabel> removed, IEnumerable<WorkflowDefinitionLabel> added, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ using Elsa.Common.Models;
|
|||
using Elsa.Models;
|
||||
using Elsa.Workflows.Api.Models;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Exceptions;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.List;
|
||||
|
||||
[PublicAPI]
|
||||
internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request, PagedListResponse<LinkedWorkflowDefinitionSummary>>
|
||||
internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker, IEnumerable<IWorkflowDefinitionFilterProvider>? filterProviders = null) : ElsaEndpoint<Request, PagedListResponse<LinkedWorkflowDefinitionSummary>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -24,12 +27,51 @@ internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker li
|
|||
{
|
||||
var pageArgs = PageArgs.FromPage(request.Page, request.PageSize);
|
||||
var filter = CreateFilter(request);
|
||||
try
|
||||
{
|
||||
foreach (var filterProvider in filterProviders ?? [])
|
||||
{
|
||||
if (!filterProvider.CanApply(filter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (EndpointSecurityOptions.SecurityIsEnabled && !HasRequiredPermissions(filterProvider, filter))
|
||||
{
|
||||
AddError("You do not have permission to filter workflow definitions by labels.");
|
||||
await Send.ErrorsAsync(StatusCodes.Status403Forbidden, cancellationToken);
|
||||
return default!;
|
||||
}
|
||||
|
||||
await filterProvider.ApplyAsync(filter, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (WorkflowDefinitionFilterNotSupportedException exception)
|
||||
{
|
||||
AddError(exception.Message);
|
||||
await Send.ErrorsAsync(StatusCodes.Status501NotImplemented, cancellationToken);
|
||||
return default!;
|
||||
}
|
||||
|
||||
if (filter.LabelIds is { Count: > 0 })
|
||||
{
|
||||
AddError("Filtering workflow definitions by labels is not supported by the configured modules.");
|
||||
await Send.ErrorsAsync(StatusCodes.Status501NotImplemented, cancellationToken);
|
||||
return default!;
|
||||
}
|
||||
|
||||
var summaries = await FindAsync(request, filter, pageArgs, cancellationToken);
|
||||
var pagedList = new PagedListResponse<WorkflowDefinitionSummary>(summaries);
|
||||
var response = linker.MapAsync(pagedList);
|
||||
return response;
|
||||
}
|
||||
|
||||
private bool HasRequiredPermissions(IWorkflowDefinitionFilterProvider filterProvider, WorkflowDefinitionFilter filter)
|
||||
{
|
||||
var evaluator = HttpContext.RequestServices.GetService<IPermissionEvaluator>() ?? PermissionEvaluator.Shared;
|
||||
return filterProvider.GetRequiredPermissions(filter).All(permission => evaluator.HasPermission(HttpContext.User, permission));
|
||||
}
|
||||
|
||||
private WorkflowDefinitionFilter CreateFilter(Request request)
|
||||
{
|
||||
var versionOptions = string.IsNullOrWhiteSpace(request.VersionOptions) ? default(VersionOptions?) : VersionOptions.FromString(request.VersionOptions);
|
||||
|
|
@ -41,7 +83,8 @@ internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker li
|
|||
SearchTerm = request.SearchTerm?.Trim(),
|
||||
MaterializerName = request.MaterializerName,
|
||||
DefinitionIds = request.DefinitionIds,
|
||||
Ids = request.Ids
|
||||
Ids = request.Ids,
|
||||
LabelIds = request.Labels
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
using Elsa.Authorization;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
|
||||
namespace Elsa.Workflows.Management;
|
||||
|
||||
/// <summary>
|
||||
/// Applies an optional workflow definition filter that is provided by another module. A provider should leave an omitted or empty criterion unchanged, clear each criterion only after successfully applying it, and report unsupported non-empty criteria explicitly.
|
||||
/// </summary>
|
||||
public interface IWorkflowDefinitionFilterProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this provider can apply criteria to the specified filter.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter to inspect.</param>
|
||||
bool CanApply(WorkflowDefinitionFilter filter);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the permissions required before this provider can inspect data for the specified filter.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter to inspect.</param>
|
||||
IEnumerable<Permission> GetRequiredPermissions(WorkflowDefinitionFilter filter) => [];
|
||||
|
||||
/// <summary>
|
||||
/// Applies the provider's criteria to the specified filter.
|
||||
/// </summary>
|
||||
/// <remarks>Criteria that remain non-empty after all providers have run are reported as unsupported by the API.</remarks>
|
||||
/// <param name="filter">The filter to update.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task ApplyAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Workflows.Management.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a requested workflow definition filter is not supported by the configured modules. The workflow-definition list endpoint reports this condition as HTTP 501.
|
||||
/// </summary>
|
||||
public class WorkflowDefinitionFilterNotSupportedException(string message) : NotSupportedException(message);
|
||||
|
|
@ -22,6 +22,11 @@ public class WorkflowDefinitionFilter
|
|||
/// </summary>
|
||||
public ICollection<string>? Ids { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter by label IDs associated with workflow definition versions. An optional filter provider resolves these IDs into version IDs before the store query is executed; any matching label ID qualifies a version, while an omitted or empty collection leaves the results unfiltered. The workflow-definition list endpoint requires label view permission for a non-empty collection.
|
||||
/// </summary>
|
||||
public ICollection<string>? LabelIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter by the ID of the workflow definition.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Labels\Elsa.Labels.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Common.Services;
|
||||
using Elsa.Labels.Contracts;
|
||||
using Elsa.Labels.Entities;
|
||||
using Elsa.Labels.Services;
|
||||
using Elsa.Models;
|
||||
using Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.List;
|
||||
using Elsa.Workflows.Api.Models;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Management.Exceptions;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Stores;
|
||||
using FastEndpoints;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Workflows.Api.UnitTests.Endpoints.WorkflowDefinitions;
|
||||
|
||||
public class WorkflowDefinitionLabelFilterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task List_WithLabelId_ReturnsMatchingVersionsAndTotalCount()
|
||||
{
|
||||
var response = await ExecuteAsync(["red"], page: 0, pageSize: 1);
|
||||
|
||||
Assert.Equal(2, response.TotalCount);
|
||||
var item = Assert.Single(response.Items);
|
||||
Assert.Contains(item.Id, new[] { "red-version", "red-second-version" });
|
||||
|
||||
var secondPage = await ExecuteAsync(["red"], page: 1, pageSize: 1);
|
||||
Assert.Equal(2, secondPage.TotalCount);
|
||||
Assert.Single(secondPage.Items);
|
||||
Assert.NotEqual(item.Id, secondPage.Items.Single().Id);
|
||||
Assert.Contains(secondPage.Items.Single().Id, new[] { "red-version", "red-second-version" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithUnknownLabelId_ReturnsNoResults()
|
||||
{
|
||||
var response = await ExecuteAsync(["unknown"]);
|
||||
|
||||
Assert.Empty(response.Items);
|
||||
Assert.Equal(0, response.TotalCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithoutLabelIds_ReturnsAllVersions()
|
||||
{
|
||||
var response = await ExecuteAsync(null, permissions: ["workflows/definitions:view"]);
|
||||
|
||||
Assert.Equal(5, response.TotalCount);
|
||||
Assert.Equal(5, response.Items.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithWorkflowPermissionWildcard_ReturnsMatchingVersions()
|
||||
{
|
||||
var response = await ExecuteAsync(["red"], permissions: ["workflows/definitions/*:view"]);
|
||||
|
||||
Assert.Equal(2, response.TotalCount);
|
||||
Assert.Equal(2, response.Items.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithMultipleLabelIds_UsesAnyMatchingLabel()
|
||||
{
|
||||
var response = await ExecuteAsync(["red", "blue"]);
|
||||
|
||||
Assert.Equal(3, response.TotalCount);
|
||||
Assert.Equal(["blue-version", "red-second-version", "red-version"], response.Items.Select(x => x.Id).Order());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithLabelIdsAndVersionIds_IntersectsBothFilters()
|
||||
{
|
||||
var response = await ExecuteAsync(["red"], ids: ["red-unlabeled-version", "red-version"]);
|
||||
|
||||
Assert.Equal(1, response.TotalCount);
|
||||
Assert.Equal("red-version", Assert.Single(response.Items).Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithLabelFilter_RequiresLabelPermissionBeforeQuerying()
|
||||
{
|
||||
var workflowDefinitionStore = Substitute.For<IWorkflowDefinitionStore>();
|
||||
ConfigureEmptyPage(workflowDefinitionStore);
|
||||
var labelStore = Substitute.For<IWorkflowDefinitionLabelStore, IWorkflowDefinitionLabelQuery>();
|
||||
var labelProvider = new WorkflowDefinitionLabelFilterProvider(labelStore);
|
||||
var endpoint = Factory.Create<List>(
|
||||
CreateHttpContext("workflows/definitions:view"),
|
||||
workflowDefinitionStore,
|
||||
new TestWorkflowDefinitionLinker(),
|
||||
new IWorkflowDefinitionFilterProvider[] { labelProvider });
|
||||
|
||||
await endpoint.ExecuteAsync(new Request { Labels = ["red"] }, CancellationToken.None);
|
||||
|
||||
Assert.Equal(StatusCodes.Status403Forbidden, endpoint.HttpContext.Response.StatusCode);
|
||||
Assert.Empty(labelStore.ReceivedCalls());
|
||||
Assert.Empty(workflowDefinitionStore.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithUnrelatedApplicableProvider_LeavesLabelFilterUnsupported()
|
||||
{
|
||||
var workflowDefinitionStore = Substitute.For<IWorkflowDefinitionStore>();
|
||||
ConfigureEmptyPage(workflowDefinitionStore);
|
||||
var endpoint = Factory.Create<List>(
|
||||
CreateHttpContext("workflows/definitions:view", "workflows/definitions/labels:view"),
|
||||
workflowDefinitionStore,
|
||||
new TestWorkflowDefinitionLinker(),
|
||||
new IWorkflowDefinitionFilterProvider[] { new UnrelatedFilterProvider() });
|
||||
|
||||
await endpoint.ExecuteAsync(new Request { Labels = ["red"] }, CancellationToken.None);
|
||||
|
||||
Assert.Equal(StatusCodes.Status501NotImplemented, endpoint.HttpContext.Response.StatusCode);
|
||||
Assert.Empty(workflowDefinitionStore.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LabelFilterProvider_WithUnsupportedStoreThrowsExplicitly()
|
||||
{
|
||||
var provider = new WorkflowDefinitionLabelFilterProvider(Substitute.For<IWorkflowDefinitionLabelStore>());
|
||||
var filter = new WorkflowDefinitionFilter { LabelIds = ["red"] };
|
||||
|
||||
await Assert.ThrowsAsync<WorkflowDefinitionFilterNotSupportedException>(() => provider.ApplyAsync(filter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithoutLabelProvider_ReturnsNotImplemented()
|
||||
{
|
||||
var store = Substitute.For<IWorkflowDefinitionStore>();
|
||||
var endpoint = Factory.Create<List>(new DefaultHttpContext(), store, new TestWorkflowDefinitionLinker(), Array.Empty<IWorkflowDefinitionFilterProvider>());
|
||||
|
||||
await endpoint.ExecuteAsync(new Request { Labels = ["red"] }, CancellationToken.None);
|
||||
|
||||
Assert.Equal(StatusCodes.Status501NotImplemented, endpoint.HttpContext.Response.StatusCode);
|
||||
Assert.Empty(store.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithUnsupportedLabelStore_ReturnsNotImplemented()
|
||||
{
|
||||
var store = Substitute.For<IWorkflowDefinitionStore>();
|
||||
var labelStore = Substitute.For<IWorkflowDefinitionLabelStore>();
|
||||
var provider = new WorkflowDefinitionLabelFilterProvider(labelStore);
|
||||
var endpoint = Factory.Create<List>(
|
||||
CreateHttpContext("workflows/definitions:view", "workflows/definitions/labels:view"),
|
||||
store,
|
||||
new TestWorkflowDefinitionLinker(),
|
||||
new IWorkflowDefinitionFilterProvider[] { provider });
|
||||
|
||||
await endpoint.ExecuteAsync(new Request { Labels = ["red"] }, CancellationToken.None);
|
||||
|
||||
Assert.Equal(StatusCodes.Status501NotImplemented, endpoint.HttpContext.Response.StatusCode);
|
||||
Assert.Empty(store.ReceivedCalls());
|
||||
}
|
||||
|
||||
private static async Task<PagedListResponse<LinkedWorkflowDefinitionSummary>> ExecuteAsync(string[]? labels, string[]? ids = null, int? page = 0, int? pageSize = null, string[]? permissions = null)
|
||||
{
|
||||
var memoryStore = new MemoryStore<WorkflowDefinition>();
|
||||
var workflowDefinitionStore = new MemoryWorkflowDefinitionStore(memoryStore);
|
||||
await workflowDefinitionStore.SaveManyAsync(
|
||||
[
|
||||
new WorkflowDefinition { Id = "red-version", DefinitionId = "red", Name = "Red", MaterializerName = "Json" },
|
||||
new WorkflowDefinition { Id = "red-unlabeled-version", DefinitionId = "red", Name = "Red", MaterializerName = "Json" },
|
||||
new WorkflowDefinition { Id = "red-second-version", DefinitionId = "red-second", Name = "Red second", MaterializerName = "Json" },
|
||||
new WorkflowDefinition { Id = "blue-version", DefinitionId = "blue", Name = "Blue", MaterializerName = "Json" },
|
||||
new WorkflowDefinition { Id = "plain-version", DefinitionId = "plain", Name = "Plain", MaterializerName = "Json" }
|
||||
]);
|
||||
|
||||
var labelStore = new InMemoryWorkflowDefinitionLabelStore(new MemoryStore<WorkflowDefinitionLabel>());
|
||||
await labelStore.SaveManyAsync(
|
||||
[
|
||||
new WorkflowDefinitionLabel { Id = "red-association", WorkflowDefinitionId = "red", WorkflowDefinitionVersionId = "red-version", LabelId = "red" },
|
||||
new WorkflowDefinitionLabel { Id = "red-second-association", WorkflowDefinitionId = "red-second", WorkflowDefinitionVersionId = "red-second-version", LabelId = "red" },
|
||||
new WorkflowDefinitionLabel { Id = "blue-association", WorkflowDefinitionId = "blue", WorkflowDefinitionVersionId = "blue-version", LabelId = "blue" }
|
||||
]);
|
||||
|
||||
var endpoint = Factory.Create<List>(
|
||||
CreateHttpContext(permissions ?? ["workflows/definitions:view", "workflows/definitions/labels:view"]),
|
||||
workflowDefinitionStore,
|
||||
new TestWorkflowDefinitionLinker(),
|
||||
new IWorkflowDefinitionFilterProvider[] { new WorkflowDefinitionLabelFilterProvider(labelStore) });
|
||||
|
||||
return await endpoint.ExecuteAsync(new Request { Labels = labels, Ids = ids, Page = page, PageSize = pageSize }, CancellationToken.None);
|
||||
}
|
||||
|
||||
private sealed class TestWorkflowDefinitionLinker : IWorkflowDefinitionLinker
|
||||
{
|
||||
public Task<LinkedWorkflowDefinitionModel> MapAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
|
||||
public PagedListResponse<LinkedWorkflowDefinitionSummary> MapAsync(PagedListResponse<WorkflowDefinitionSummary> list, CancellationToken cancellationToken = default) => new()
|
||||
{
|
||||
Items = list.Items.Select(x => new LinkedWorkflowDefinitionSummary
|
||||
{
|
||||
Id = x.Id,
|
||||
DefinitionId = x.DefinitionId,
|
||||
Name = x.Name,
|
||||
Version = x.Version
|
||||
}).ToList(),
|
||||
TotalCount = list.TotalCount
|
||||
};
|
||||
|
||||
public Task<List<LinkedWorkflowDefinitionModel>> MapAsync(List<WorkflowDefinition> definitions, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private sealed class UnrelatedFilterProvider : IWorkflowDefinitionFilterProvider
|
||||
{
|
||||
public bool CanApply(WorkflowDefinitionFilter filter) => true;
|
||||
|
||||
public Task ApplyAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static DefaultHttpContext CreateHttpContext(params string[] permissions) => new()
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(permissions.Select(x => new Claim(PermissionNames.ClaimType, x)), "test"))
|
||||
};
|
||||
|
||||
private static void ConfigureEmptyPage(IWorkflowDefinitionStore store) =>
|
||||
store.FindSummariesAsync(
|
||||
Arg.Any<WorkflowDefinitionFilter>(),
|
||||
Arg.Any<WorkflowDefinitionOrder<string>>(),
|
||||
Arg.Any<PageArgs>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(Page.Empty<WorkflowDefinitionSummary>()));
|
||||
}
|
||||
Loading…
Reference in a new issue