Issue #21: implement worker bootstrap and options

This commit is contained in:
Joseph Doherty
2026-04-26 16:53:06 -04:00
parent e2b4dfcb32
commit 0af1427859
19 changed files with 736 additions and 2 deletions
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Tests.Bootstrap;
internal sealed class MemoryWorkerEnvironment : IWorkerEnvironment
{
private readonly Dictionary<string, string> _values = new();
private readonly Exception? _exception;
public MemoryWorkerEnvironment()
{
}
public MemoryWorkerEnvironment(Exception exception)
{
_exception = exception;
}
public void Set(string name, string value)
{
_values[name] = value;
}
public string? GetEnvironmentVariable(string name)
{
if (_exception is not null)
{
throw _exception;
}
return _values.TryGetValue(name, out string value)
? value
: null;
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace MxGateway.Worker.Tests.Bootstrap;
internal sealed class MemoryWorkerLogEntry
{
public MemoryWorkerLogEntry(
string level,
string eventName,
IReadOnlyDictionary<string, object?> fields)
{
Level = level;
EventName = eventName;
Fields = fields;
}
public string Level { get; }
public string EventName { get; }
public IReadOnlyDictionary<string, object?> Fields { get; }
}
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Tests.Bootstrap;
internal sealed class MemoryWorkerLogger : IWorkerLogger
{
public List<MemoryWorkerLogEntry> Entries { get; } = new();
public void Information(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Entries.Add(new MemoryWorkerLogEntry("Information", eventName, WorkerLogRedactor.RedactFields(fields)));
}
public void Error(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Entries.Add(new MemoryWorkerLogEntry("Error", eventName, WorkerLogRedactor.RedactFields(fields)));
}
}
@@ -0,0 +1,113 @@
using System;
using MxGateway.Contracts;
using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Tests.Bootstrap;
public sealed class WorkerApplicationTests
{
[Fact]
public void Run_WithValidBootstrapArguments_ReturnsSuccessAndLogsRedactedNonce()
{
MemoryWorkerEnvironment environment = CreateEnvironment("nonce-secret");
MemoryWorkerLogger logger = new();
int exitCode = MxGateway.Worker.WorkerApplication.Run(
ValidArgs(),
environment,
logger);
Assert.Equal((int)WorkerExitCode.Success, exitCode);
MemoryWorkerLogEntry entry = Assert.Single(logger.Entries);
Assert.Equal("Information", entry.Level);
Assert.Equal("WorkerBootstrapSucceeded", entry.EventName);
Assert.Equal("session-1", entry.Fields["session_id"]);
Assert.Equal("mxaccess-gateway-123-session-1", entry.Fields["pipe_name"]);
Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, entry.Fields["protocol_version"]);
Assert.Equal("[redacted]", entry.Fields["nonce"]);
}
[Fact]
public void Run_WithMissingRequiredArguments_ReturnsInvalidArguments()
{
MemoryWorkerEnvironment environment = CreateEnvironment("nonce-secret");
MemoryWorkerLogger logger = new();
int exitCode = MxGateway.Worker.WorkerApplication.Run(
[],
environment,
logger);
Assert.Equal((int)WorkerExitCode.InvalidArguments, exitCode);
MemoryWorkerLogEntry entry = Assert.Single(logger.Entries);
Assert.Equal("Error", entry.Level);
Assert.Equal("WorkerBootstrapFailed", entry.EventName);
Assert.Equal(WorkerExitCode.InvalidArguments, entry.Fields["exit_code"]);
}
[Fact]
public void Run_WithInvalidProtocolVersion_ReturnsInvalidProtocolVersion()
{
MemoryWorkerEnvironment environment = CreateEnvironment("nonce-secret");
MemoryWorkerLogger logger = new();
int exitCode = MxGateway.Worker.WorkerApplication.Run(
ValidArgs(protocolVersion: "999"),
environment,
logger);
Assert.Equal((int)WorkerExitCode.InvalidProtocolVersion, exitCode);
}
[Fact]
public void Run_WithMissingNonce_ReturnsMissingNonce()
{
MemoryWorkerEnvironment environment = new();
MemoryWorkerLogger logger = new();
int exitCode = MxGateway.Worker.WorkerApplication.Run(
ValidArgs(),
environment,
logger);
Assert.Equal((int)WorkerExitCode.MissingNonce, exitCode);
}
[Fact]
public void Run_WithUnexpectedBootstrapFailure_ReturnsUnexpectedFailure()
{
MemoryWorkerEnvironment environment = new(new InvalidOperationException("environment failed"));
MemoryWorkerLogger logger = new();
int exitCode = MxGateway.Worker.WorkerApplication.Run(
ValidArgs(),
environment,
logger);
Assert.Equal((int)WorkerExitCode.UnexpectedFailure, exitCode);
MemoryWorkerLogEntry entry = Assert.Single(logger.Entries);
Assert.Equal("WorkerBootstrapUnexpectedFailure", entry.EventName);
Assert.Equal(WorkerExitCode.UnexpectedFailure, entry.Fields["exit_code"]);
Assert.Equal(typeof(InvalidOperationException).FullName, entry.Fields["exception_type"]);
}
private static string[] ValidArgs(string? protocolVersion = null)
{
return
[
"--session-id",
"session-1",
"--pipe-name",
"mxaccess-gateway-123-session-1",
"--protocol-version",
protocolVersion ?? GatewayContractInfo.WorkerProtocolVersion.ToString(),
];
}
private static MemoryWorkerEnvironment CreateEnvironment(string nonce)
{
MemoryWorkerEnvironment environment = new();
environment.Set(WorkerOptions.NonceEnvironmentVariableName, nonce);
return environment;
}
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.IO;
using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Tests.Bootstrap;
public sealed class WorkerConsoleLoggerTests
{
[Fact]
public void Information_RedactsNonceInStructuredOutput()
{
StringWriter writer = new();
WorkerConsoleLogger logger = new(writer);
logger.Information("WorkerBootstrapSucceeded", new Dictionary<string, object?>
{
["session_id"] = "session-1",
["nonce"] = "nonce-secret",
});
string output = writer.ToString();
Assert.Contains("event=WorkerBootstrapSucceeded", output);
Assert.Contains("session_id=session-1", output);
Assert.Contains("nonce=[redacted]", output);
Assert.DoesNotContain("nonce-secret", output);
}
}
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Tests.Bootstrap;
public sealed class WorkerLogRedactorTests
{
[Fact]
public void RedactFields_RedactsNonceSecretPasswordTokenCredentialAndApiKeyFields()
{
Dictionary<string, object?> fields = new()
{
["nonce"] = "nonce-secret",
["client_secret"] = "secret",
["password"] = "password",
["auth_token"] = "token",
["credential_value"] = "credential",
["api_key"] = "key",
["session_id"] = "session-1",
};
Dictionary<string, object?> redacted = WorkerLogRedactor.RedactFields(fields);
Assert.Equal("[redacted]", redacted["nonce"]);
Assert.Equal("[redacted]", redacted["client_secret"]);
Assert.Equal("[redacted]", redacted["password"]);
Assert.Equal("[redacted]", redacted["auth_token"]);
Assert.Equal("[redacted]", redacted["credential_value"]);
Assert.Equal("[redacted]", redacted["api_key"]);
Assert.Equal("session-1", redacted["session_id"]);
}
}
@@ -0,0 +1,115 @@
using MxGateway.Contracts;
using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Tests.Bootstrap;
public sealed class WorkerOptionsParserTests
{
[Fact]
public void Parse_WithAllRequiredInputs_ReturnsWorkerOptions()
{
WorkerOptionsParser parser = new(CreateEnvironment("nonce-secret"));
WorkerBootstrapResult result = parser.Parse(ValidArgs());
Assert.True(result.Succeeded);
Assert.Equal(WorkerExitCode.Success, result.ExitCode);
Assert.NotNull(result.Options);
Assert.Equal("session-1", result.Options.SessionId);
Assert.Equal("mxaccess-gateway-123-session-1", result.Options.PipeName);
Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, result.Options.ProtocolVersion);
Assert.Equal("nonce-secret", result.Options.Nonce);
}
[Fact]
public void Parse_WithMissingSessionId_ReturnsInvalidArguments()
{
WorkerOptionsParser parser = new(CreateEnvironment("nonce-secret"));
WorkerBootstrapResult result = parser.Parse(
[
"--pipe-name",
"mxaccess-gateway-123-session-1",
"--protocol-version",
GatewayContractInfo.WorkerProtocolVersion.ToString(),
]);
Assert.False(result.Succeeded);
Assert.Equal(WorkerExitCode.InvalidArguments, result.ExitCode);
Assert.Contains(result.Errors, error => error.Contains("--session-id"));
}
[Fact]
public void Parse_WithUnknownOption_ReturnsInvalidArguments()
{
WorkerOptionsParser parser = new(CreateEnvironment("nonce-secret"));
WorkerBootstrapResult result = parser.Parse(
[
"--session-id",
"session-1",
"--pipe-name",
"mxaccess-gateway-123-session-1",
"--protocol-version",
GatewayContractInfo.WorkerProtocolVersion.ToString(),
"--unexpected",
"value",
]);
Assert.Equal(WorkerExitCode.InvalidArguments, result.ExitCode);
Assert.Contains(result.Errors, error => error.Contains("Unknown option"));
}
[Fact]
public void Parse_WithNonNumericProtocolVersion_ReturnsInvalidProtocolVersion()
{
WorkerOptionsParser parser = new(CreateEnvironment("nonce-secret"));
WorkerBootstrapResult result = parser.Parse(ValidArgs(protocolVersion: "abc"));
Assert.False(result.Succeeded);
Assert.Equal(WorkerExitCode.InvalidProtocolVersion, result.ExitCode);
}
[Fact]
public void Parse_WithUnsupportedProtocolVersion_ReturnsInvalidProtocolVersion()
{
WorkerOptionsParser parser = new(CreateEnvironment("nonce-secret"));
WorkerBootstrapResult result = parser.Parse(ValidArgs(protocolVersion: "999"));
Assert.False(result.Succeeded);
Assert.Equal(WorkerExitCode.InvalidProtocolVersion, result.ExitCode);
}
[Fact]
public void Parse_WithMissingNonce_ReturnsMissingNonce()
{
WorkerOptionsParser parser = new(new MemoryWorkerEnvironment());
WorkerBootstrapResult result = parser.Parse(ValidArgs());
Assert.False(result.Succeeded);
Assert.Equal(WorkerExitCode.MissingNonce, result.ExitCode);
}
private static string[] ValidArgs(string? protocolVersion = null)
{
return
[
"--session-id",
"session-1",
"--pipe-name",
"mxaccess-gateway-123-session-1",
"--protocol-version",
protocolVersion ?? GatewayContractInfo.WorkerProtocolVersion.ToString(),
];
}
private static MemoryWorkerEnvironment CreateEnvironment(string nonce)
{
MemoryWorkerEnvironment environment = new();
environment.Set(WorkerOptions.NonceEnvironmentVariableName, nonce);
return environment;
}
}