From 17447af85bcad0fe4930ae45c68fbac10a9848e0 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Thu, 8 Oct 2026 03:10:16 -0600 Subject: [PATCH] Test non-201 answers, sender resilience, trimming and ParamName Four characterization tests for behaviour that had none: a 200 is logged as a failure like any status but 201; a 500 does not stop the sending thread from delivering the reports behind it; the constructor trims the base URL, application and key before they reach the wire; and each ArgumentException names the parameter it rejects. Co-Authored-By: Claude Opus 5.5 --- tests/TraceClient.Tests/TraceClientTest.cs | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/TraceClient.Tests/TraceClientTest.cs b/tests/TraceClient.Tests/TraceClientTest.cs index 92b6300..ad8e162 100644 --- a/tests/TraceClient.Tests/TraceClientTest.cs +++ b/tests/TraceClient.Tests/TraceClientTest.cs @@ -137,6 +137,60 @@ public void Report_DoesNotThrowWhenTheServerRejectsTheKey() } } + [Fact] + public void Report_TreatsAnyStatusButExactly201AsAFailure() + { + // The wire format promises 201 is success; another 2xx is not. + using (var ok = new StubServer(status: () => 200)) + { + var log = new ConcurrentQueue(); + var client = new TraceClient(ok.BaseUrl, "MyGame", "1.2.3", key: "k", log: log.Enqueue); + + client.Report("startup"); + client.Close(); + + Assert.Single(ok.Received); + Assert.Contains(log, m => m.Contains("answered 200")); + } + } + + [Fact] + public void Report_KeepsSendingAfterAFailedDelivery() + { + // One rejected report must not stop the sending thread: the first + // answer is a 500, every later one a 201. + int answered = 0; + using (var flaky = new StubServer(status: () => Interlocked.Increment(ref answered) == 1 ? 500 : 201)) + { + var log = new ConcurrentQueue(); + var client = new TraceClient(flaky.BaseUrl, "MyGame", "1.2.3", key: "k", log: log.Enqueue); + + client.Report("first"); + client.Report("second"); + client.Report("third"); + client.Close(); + + Assert.Equal(new[] { "first", "second", "third" }, + flaky.Received.Select(r => r.Body.Split('"')[7]).ToArray()); + Assert.Single(log, m => m.Contains("answered 500")); + } + } + + [Fact] + public void Constructor_TrimsTheBaseUrlApplicationAndKey() + { + var client = new TraceClient(" " + _server.BaseUrl + "/ ", " MyGame ", "1.2.3", key: " k-123 "); + + client.Report("startup"); + client.Close(); + + Received request = _server.Received.Single(); + Assert.Equal("/api/metrics", request.Path); + Assert.Equal("Bearer k-123", request.Authorization); + Assert.Equal("trace-client/" + TraceClient.Version + " (MyGame)", request.UserAgent); + Assert.StartsWith("{\"application\":\"MyGame\",", request.Body); + } + [Fact] public void Report_NeverThrowsEvenWhenTheLoggerDoes() { @@ -274,6 +328,19 @@ public void Constructor_RejectsAMissingBaseUrlOrApplication() Assert.Throws(() => new TraceClient("http://x", "", "1.2.3")); } + [Fact] + public void Constructor_NamesTheParameterItRejects() + { + // A programming error should point at the argument to fix. + Assert.Equal("baseUrl", Assert.Throws(() => new TraceClient(" ", "MyGame", "1.2.3")).ParamName); + Assert.Equal("application", Assert.Throws(() => new TraceClient("http://x", " ", "1.2.3")).ParamName); + Assert.Equal("version", Assert.Throws(() => new TraceClient("http://x", "MyGame", null)).ParamName); + Assert.Equal("version", Assert.Throws( + () => new TraceClient("http://x", "MyGame", new string('9', TraceClient.MaxLength + 1))).ParamName); + Assert.Equal("installId", Assert.Throws( + () => new TraceClient("http://x", "MyGame", "1.2.3", installId: new string('i', TraceClient.MaxLength + 1))).ParamName); + } + [Fact] public void Constructor_RejectsAMalformedBaseUrlWithAnArgumentException() {