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(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 RunAsync( TestClientOptions options, TestClientOutput output, TextReader input, CancellationToken cancellationToken) { Options = options; output.Write("fake.completed", "complete", phase: "test"); return Task.FromResult(exitCode); } } }