Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/TestClient/TestClientCommandTests.cs
T
KyuubiYoru 7e3be2cad1
quality-gate / quality (push) Failing after 1m3s
feat(tooling): add standalone rendezvous test client (#25)
2026-07-16 11:05:56 +02:00

218 lines
8.2 KiB
C#

using System.Text.Json;
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.TestClient;
namespace FinalFactory.Rendezvous.Tests.TestClient;
public sealed class TestClientCommandTests
{
[Fact]
public void HostOptionsParseBoundedPublicConfigurationWithoutAcceptingASecretArgument()
{
TestClientParseResult parsed = TestClientOptionParser.Parse(
[
"host",
"--service", "https://rendezvous.example/base",
"--mediator", "127.0.0.1:9050",
"--game", "space-game",
"--environment", "production",
"--region", "eu-central",
"--protocol", "7",
"--metadata", "mode=online-coop",
"--fallback", "203.0.113.50:7777",
"--publisher-credential-env", "TEST_PUBLISHER_CREDENTIAL",
"--script",
"--json",
"--exit-after-echo",
]);
Assert.True(parsed.Succeeded, parsed.Error);
TestClientOptions options = Assert.IsType<TestClientOptions>(parsed.Options);
Assert.Equal(TestClientMode.Host, options.Mode);
Assert.Equal(new Uri("https://rendezvous.example/base/"), options.ServiceUri);
Assert.Equal(7u, options.ProtocolVersion);
Assert.Equal("online-coop", options.Metadata["mode"]);
Assert.Equal("203.0.113.50", options.DedicatedFallback?.Address);
Assert.Equal(7777, options.DedicatedFallback?.Port);
Assert.Equal("TEST_PUBLISHER_CREDENTIAL", options.PublisherCredentialEnvironmentVariable);
Assert.True(options.Script);
Assert.True(options.Json);
Assert.True(options.ExitAfterEcho);
Assert.Equal(TimeSpan.FromSeconds(20), options.RunDuration);
TestClientParseResult secret = TestClientOptionParser.Parse(
["host", "--publisher-credential", "secret-canary"]);
Assert.False(secret.Succeeded);
Assert.Contains("Unknown option", secret.Error, StringComparison.Ordinal);
}
[Fact]
public void ScriptExitCodesRemainStable()
{
Assert.Equal(0, (int)TestClientExitCode.Success);
Assert.Equal(2, (int)TestClientExitCode.Usage);
Assert.Equal(3, (int)TestClientExitCode.Configuration);
Assert.Equal(10, (int)TestClientExitCode.ServiceFailure);
Assert.Equal(11, (int)TestClientExitCode.NoCompatibleSession);
Assert.Equal(12, (int)TestClientExitCode.TraversalFailed);
Assert.Equal(13, (int)TestClientExitCode.DirectTrafficFailed);
Assert.Equal(130, (int)TestClientExitCode.Cancelled);
}
[Fact]
public void HostFailureBudgetStopsAuthorityLossAndBoundsTransientRetries()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
HostServiceFailureBudget authority = new();
Assert.True(authority.ShouldStop(
RendezvousErrorCode.NotFound,
now.AddMinutes(1),
now));
HostServiceFailureBudget transient = new();
Assert.False(transient.ShouldStop(
RendezvousErrorCode.ServiceUnavailable,
now.AddMinutes(1),
now));
Assert.False(transient.ShouldStop(
RendezvousErrorCode.RateLimited,
now.AddMinutes(1),
now));
Assert.True(transient.ShouldStop(
RendezvousErrorCode.InternalError,
now.AddMinutes(1),
now));
transient.Reset();
Assert.True(transient.ShouldStop(
RendezvousErrorCode.ServiceUnavailable,
now,
now));
}
[Theory]
[InlineData("https://user:password@rendezvous.example/")]
[InlineData("file:///tmp/rendezvous")]
[InlineData("https://rendezvous.example/?token=secret")]
public void ServiceUrlRejectsCredentialAndNonHttpShapes(string url)
{
TestClientParseResult parsed = TestClientOptionParser.Parse(["browse", "--service", url]);
Assert.False(parsed.Succeeded);
Assert.Contains("service URL", parsed.Error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ApplicationRoutesParsedOptionsThroughTheInjectableUiFlow()
{
FakeCommandRunner runner = new(TestClientExitCode.NoCompatibleSession);
TestClientApplication application = new(runner);
StringWriter output = new();
StringWriter error = new();
int exitCode = await application.RunAsync(
["browse", "--script", "--json"],
new StringReader(string.Empty),
output,
error,
CancellationToken.None);
Assert.Equal((int)TestClientExitCode.NoCompatibleSession, exitCode);
Assert.NotNull(runner.Options);
Assert.Equal(TestClientMode.Browse, runner.Options.Mode);
Assert.True(runner.Options.Script);
using JsonDocument item = JsonDocument.Parse(output.ToString());
Assert.Equal(1, item.RootElement.GetProperty("version").GetInt32());
Assert.Equal("fake.completed", item.RootElement.GetProperty("event").GetString());
Assert.Equal(string.Empty, error.ToString());
}
[Fact]
public async Task InvalidArgumentsFailBeforeTheRunnerAndDoNotEchoTheValue()
{
FakeCommandRunner runner = new(TestClientExitCode.Success);
TestClientApplication application = new(runner);
StringWriter output = new();
StringWriter error = new();
int exitCode = await application.RunAsync(
["host", "--publisher-credential", "secret-canary"],
new StringReader(string.Empty),
output,
error,
CancellationToken.None);
Assert.Equal((int)TestClientExitCode.Usage, exitCode);
Assert.Null(runner.Options);
Assert.DoesNotContain("secret-canary", error.ToString(), StringComparison.Ordinal);
}
[Theory]
[InlineData("host", "--json", "--unknown", "value", "cli.usage", 2)]
[InlineData("host", "--json", "--help", "", "cli.help", 0)]
public async Task JsonModeKeepsHelpAndUsageFailuresMachineReadable(
string mode,
string json,
string option,
string value,
string expectedEvent,
int expectedExit)
{
FakeCommandRunner runner = new(TestClientExitCode.Success);
TestClientApplication application = new(runner);
StringWriter output = new();
StringWriter error = new();
string[] args = string.IsNullOrEmpty(value)
? [mode, json, option]
: [mode, json, option, value];
int exitCode = await application.RunAsync(
args,
new StringReader(string.Empty),
output,
error,
CancellationToken.None);
Assert.Equal(expectedExit, exitCode);
string jsonLine = expectedExit == 0 ? output.ToString() : error.ToString();
using JsonDocument item = JsonDocument.Parse(jsonLine);
Assert.Equal(expectedEvent, item.RootElement.GetProperty("event").GetString());
}
[Fact]
public void HumanOutputNeutralizesControlCharactersFromPublicListingText()
{
StringWriter output = new();
TestClientOutput sink = new(output, new StringWriter(), json: false);
sink.Write(
"browse.session",
"available",
displayName: "host\nforged-line\u001b[31m outcome=connected\u2028next\u2029line\u202eright");
string line = output.ToString();
Assert.Equal(1, line.Count(static character => character == '\n'));
Assert.DoesNotContain('\u001b', line);
Assert.DoesNotContain('\u2028', line);
Assert.DoesNotContain('\u2029', line);
Assert.DoesNotContain('\u202e', line);
Assert.Contains("name=\"host?forged-line?[31m outcome=connected?next?line?right\"", line, StringComparison.Ordinal);
}
private sealed class FakeCommandRunner(TestClientExitCode exitCode) : ITestClientCommandRunner
{
internal TestClientOptions? Options { get; private set; }
public Task<TestClientExitCode> RunAsync(
TestClientOptions options,
TestClientOutput output,
TextReader input,
CancellationToken cancellationToken)
{
Options = options;
output.Write("fake.completed", "complete", phase: "test");
return Task.FromResult(exitCode);
}
}
}