w4c-workflows-api/w4c-workflows-api.Tests/RemoteServerExecutorTests.cs
Vitali sharp8n 42ffcb9adc workflows
2026-09-13 19:28:47 +03:00

115 lines
4.4 KiB
C#

using System.Net;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using w4c_workflows.Services.Execution;
using Xunit;
namespace w4c_workflows.Tests;
/// <summary>
/// The remote (SSH) executor stages the task script on the target server. These
/// tests pin the generated shell: the staged file must be removed on every
/// completion path and the interpreter's exit code must still be propagated.
/// </summary>
public class RemoteServerExecutorTests
{
private sealed class CapturingHandler : HttpMessageHandler
{
public string? LastBody { get; private set; }
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Content != null)
LastBody = await request.Content.ReadAsStringAsync(cancellationToken);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
"{\"exitCode\":0,\"stdout\":\"ok\",\"stderr\":\"\",\"timedOut\":false}",
Encoding.UTF8, "application/json"),
};
}
}
private sealed class Factory : IHttpClientFactory
{
private readonly HttpMessageHandler _handler;
public Factory(HttpMessageHandler handler) => _handler = handler;
public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false);
}
[Fact]
public async Task Staged_script_is_removed_and_exit_code_propagated()
{
var workDir = Directory.CreateTempSubdirectory("w4c-remote-");
try
{
await File.WriteAllTextAsync(Path.Combine(workDir.FullName, "main.sh"), "echo hi");
var handler = new CapturingHandler();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Workflows:ServerApiUrl"] = "http://server",
})
.Build();
var executor = new RemoteServerExecutor(
new Factory(handler), config, NullLogger<RemoteServerExecutor>.Instance);
var invocation = new TaskInvocation(
TaskInvocation.TypeValue, "run-1", "task-12345678", "key", "shell", "main.sh", null,
new Dictionary<string, string>(), null, workDir.FullName, "tenant-1", 1, "srv-1");
var result = await executor.ExecuteAsync(invocation, default);
Assert.True(result.Success);
Assert.NotNull(handler.LastBody);
Assert.Contains(".w4c-run-", handler.LastBody);
Assert.Contains("rm -f", handler.LastBody);
Assert.Contains("exit $rc", handler.LastBody);
}
finally
{
workDir.Delete(recursive: true);
}
}
[Fact]
public async Task Typescript_is_rejected_for_remote_with_a_clear_error()
{
var workDir = Directory.CreateTempSubdirectory("w4c-remote-ts-");
try
{
await File.WriteAllTextAsync(
Path.Combine(workDir.FullName, "main.ts"), "const value: number = 1;");
var handler = new CapturingHandler();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Workflows:ServerApiUrl"] = "http://server",
})
.Build();
var executor = new RemoteServerExecutor(
new Factory(handler), config, NullLogger<RemoteServerExecutor>.Instance);
var invocation = new TaskInvocation(
TaskInvocation.TypeValue, "run-1", "task-12345678", "key", "typescript", "main.ts", null,
new Dictionary<string, string>(), null, workDir.FullName, "tenant-1", 1, "srv-1");
var result = await executor.ExecuteAsync(invocation, default);
Assert.False(result.Success);
Assert.NotNull(result.Error);
Assert.Contains("typescript", result.Error);
Assert.Contains("not supported", result.Error);
// Raw TypeScript must never be shipped to a remote `node`: the local
// path bundles it with esbuild first, and nothing is sent here.
Assert.Null(handler.LastBody);
}
finally
{
workDir.Delete(recursive: true);
}
}
}