Merge remote-tracking branch 'origin/codex/workflow-secret-references-s4' into codex/workflow-secret-references-s5
This commit is contained in:
commit
4a6db8aa98
15
Elsa.sln
15
Elsa.sln
|
|
@ -355,6 +355,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTeleme
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "secrets", "secrets", "{D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.JavaScript", "src\modules\Elsa.Secrets.JavaScript\Elsa.Secrets.JavaScript.csproj", "{80F7ED38-15BA-4C82-88C8-12135B33F7EF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -1431,6 +1433,18 @@ Global
|
|||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x64.Build.0 = Release|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -1558,6 +1572,7 @@ Global
|
|||
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||
{D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{09B4B78B-FE02-44E2-8667-E182AF921C54} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||
{80F7ED38-15BA-4C82-88C8-12135B33F7EF} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides JavaScript expression functions for resolving Elsa secrets.
|
||||
</Description>
|
||||
<PackageTags>elsa module secrets javascript workflow security</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Expressions.JavaScript\Elsa.Expressions.JavaScript.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Secrets\Elsa.Secrets.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Secrets.JavaScript.Features;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
public static IModule UseSecretsJavaScript(this IModule module, Action<SecretsJavaScriptFeature>? configure = null)
|
||||
{
|
||||
return module.Use(configure);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using Elsa.Expressions.JavaScript.Extensions;
|
||||
using Elsa.Expressions.JavaScript.Features;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Secrets.Features;
|
||||
using Elsa.Secrets.JavaScript.Scripting.JavaScript;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Secrets.JavaScript.Features;
|
||||
|
||||
[DependsOn(typeof(JavaScriptFeature))]
|
||||
[DependsOn(typeof(SecretsFeature))]
|
||||
public class SecretsJavaScriptFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
public override void Apply()
|
||||
{
|
||||
Services
|
||||
.AddNotificationHandler<SecretsJavaScriptHandler>()
|
||||
.AddFunctionDefinitionProvider<SecretsJavaScriptHandler>();
|
||||
}
|
||||
}
|
||||
4
src/modules/Elsa.Secrets.JavaScript/FodyWeavers.xml
Normal file
4
src/modules/Elsa.Secrets.JavaScript/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait ContinueOnCapturedContext="false" />
|
||||
</Weavers>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using Elsa.Expressions.JavaScript.Notifications;
|
||||
using Elsa.Expressions.JavaScript.TypeDefinitions.Abstractions;
|
||||
using Elsa.Expressions.JavaScript.TypeDefinitions.Models;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Secrets.Contracts;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Secrets.JavaScript.Scripting.JavaScript;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class SecretsJavaScriptHandler(ISecretResolver secretResolver) : FunctionDefinitionProvider, INotificationHandler<EvaluatingJavaScript>
|
||||
{
|
||||
public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = notification.Context;
|
||||
notification.Engine.SetValue("getSecret", (Func<string, Task<string>>)(name => secretResolver.ResolveAsync(name, context.CancellationToken)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override IEnumerable<FunctionDefinition> GetFunctionDefinitions(TypeDefinitionContext context)
|
||||
{
|
||||
yield return CreateFunctionDefinition(function => function
|
||||
.Name("getSecret")
|
||||
.ReturnType("Promise<string>")
|
||||
.Parameter("name", "string"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using CShells.Features;
|
||||
using Elsa.Expressions.JavaScript.Extensions;
|
||||
using Elsa.Expressions.JavaScript.ShellFeatures;
|
||||
using Elsa.Secrets.JavaScript.Scripting.JavaScript;
|
||||
using Elsa.Secrets.ShellFeatures;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Secrets.JavaScript.ShellFeatures;
|
||||
|
||||
[ShellFeature(
|
||||
DisplayName = "Secrets JavaScript",
|
||||
Description = "Provides JavaScript expression functions for resolving Elsa secrets.",
|
||||
DependsOn = [typeof(JavaScriptFeature), typeof(SecretsFeature)])]
|
||||
[UsedImplicitly]
|
||||
public class SecretsJavaScriptFeature : IShellFeature
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddNotificationHandler<SecretsJavaScriptHandler>()
|
||||
.AddFunctionDefinitionProvider<SecretsJavaScriptHandler>();
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Expressions.JavaScript\Elsa.Expressions.JavaScript.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Secrets.JavaScript\Elsa.Secrets.JavaScript.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
using Elsa.Expressions.JavaScript.Contracts;
|
||||
using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts;
|
||||
using Elsa.Expressions.JavaScript.TypeDefinitions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Secrets.Contracts;
|
||||
using Elsa.Secrets.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.JavaScript.IntegrationTests;
|
||||
|
||||
public class SecretsJavaScriptTests
|
||||
{
|
||||
private readonly TestSecretResolver _secretResolver = new();
|
||||
private readonly WorkflowTestFixture _fixture;
|
||||
|
||||
public SecretsJavaScriptTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_fixture = new(testOutputHelper);
|
||||
_fixture
|
||||
.ConfigureServices(services => services.AddSingleton<ISecretResolver>(_secretResolver))
|
||||
.ConfigureElsa(ConfigureElsa);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecret_ReturnsPromiseCompatibleSecretValue()
|
||||
{
|
||||
var result = await EvaluateScriptAsync<string>("return getSecret('api:key');");
|
||||
|
||||
Assert.Equal("secret-value", result);
|
||||
Assert.Equal(["api:key"], _secretResolver.ResolvedNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecret_ComposesWithThen()
|
||||
{
|
||||
var result = await EvaluateScriptAsync<string>("return getSecret('api:key').then(value => value + '-suffix');");
|
||||
|
||||
Assert.Equal("secret-value-suffix", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecret_ComposesWithAsyncIife()
|
||||
{
|
||||
var result = await EvaluateScriptAsync<string>("return (async () => await getSecret('api:key'))();");
|
||||
|
||||
Assert.Equal("secret-value", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TypeDefinitions_DocumentGetSecretAsPromiseOfString()
|
||||
{
|
||||
var typeDefinitions = await GenerateTypeDefinitionsAsync();
|
||||
|
||||
Assert.Contains("declare function getSecret(name: string): Promise<string>;", typeDefinitions);
|
||||
}
|
||||
|
||||
private static void ConfigureElsa(IModule module)
|
||||
{
|
||||
module.UseSecretsJavaScript();
|
||||
}
|
||||
|
||||
private async Task<T> EvaluateScriptAsync<T>(string script)
|
||||
{
|
||||
var context = await _fixture.CreateExpressionExecutionContextAsync();
|
||||
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
var result = await evaluator.EvaluateAsync(script, typeof(T), context);
|
||||
|
||||
return result is T typedResult ? typedResult : (T)Convert.ChangeType(result, typeof(T))!;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateTypeDefinitionsAsync()
|
||||
{
|
||||
await _fixture.BuildAsync();
|
||||
var workflow = new Workflow();
|
||||
var workflowGraphBuilder = _fixture.Services.GetRequiredService<IWorkflowGraphBuilder>();
|
||||
var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
|
||||
var typeDefinitionContext = new TypeDefinitionContext(workflowGraph, null, null, CancellationToken.None);
|
||||
var typeDefinitionService = _fixture.Services.GetRequiredService<ITypeDefinitionService>();
|
||||
|
||||
return await typeDefinitionService.GenerateTypeDefinitionsAsync(typeDefinitionContext);
|
||||
}
|
||||
|
||||
private class TestSecretResolver : ISecretResolver
|
||||
{
|
||||
private readonly Dictionary<string, string> _secrets = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["api:key"] = "secret-value"
|
||||
};
|
||||
|
||||
public List<string> ResolvedNames { get; } = [];
|
||||
|
||||
public Task<string> ResolveAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ResolvedNames.Add(name);
|
||||
return Task.FromResult(_secrets[name]);
|
||||
}
|
||||
|
||||
public Task<string> ResolveAsync(SecretReference reference, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ResolveAsync(reference.Name, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue