From ec2e13264ae7811dd6a27571f58e6bb705312a9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:31:51 +0000 Subject: [PATCH 1/5] Initial plan From da9b00139ecd9c096ec0db6d55ba55ac43f59ebd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:48:21 +0000 Subject: [PATCH 2/5] Move non-JSON tests from Newtonsoft.Json.Test to core test project Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- .../MovieReviews/IMovieReviewService.cs | 59 ---- .../RestClientTests.cs | 308 ------------------ .../MovieReviews/ErrorResponse.cs | 15 + .../MovieReviews/IMovieReviewService.cs | 68 ++++ .../NonJsonRestClientTests.cs | 267 +++++++++++++++ 5 files changed, 350 insertions(+), 367 deletions(-) create mode 100644 Activout.RestClient.Test/MovieReviews/ErrorResponse.cs create mode 100644 Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs create mode 100644 Activout.RestClient.Test/NonJsonRestClientTests.cs diff --git a/Activout.RestClient.Newtonsoft.Json.Test/MovieReviews/IMovieReviewService.cs b/Activout.RestClient.Newtonsoft.Json.Test/MovieReviews/IMovieReviewService.cs index 6fb1c6f..07b147b 100644 --- a/Activout.RestClient.Newtonsoft.Json.Test/MovieReviews/IMovieReviewService.cs +++ b/Activout.RestClient.Newtonsoft.Json.Test/MovieReviews/IMovieReviewService.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -25,13 +23,6 @@ public interface IMovieReviewService [Get("/{movieId}/reviews/{reviewId}")] Review GetReview(string movieId, string reviewId); - [Delete("/{movieId}/reviews/{reviewId}")] - void DeleteReview(string movieId, string reviewId); - - [Get("/fail")] - [ErrorResponse(typeof(byte[]))] - void Fail(); - [Post] [Path("/{movieId}/reviews")] Task SubmitReview([PathParam("movieId")] string movieId, Review review); @@ -44,65 +35,15 @@ public interface IMovieReviewService [Path("/{movieId}/reviews/{reviewId}")] Review PartialUpdateReview(string movieId, [PathParam] string reviewId, Review review); - [Post("/import.csv")] - [ContentType("text/csv")] - Task Import(string csv); - [Get] Task> QueryMoviesByDate( [QueryParam] DateTime begin, [QueryParam] DateTime end); - HttpContent GetHttpContent(); - - HttpResponseMessage GetHttpResponseMessage(); - [Path("/object")] JObject GetJObject(); [Path("/array")] Task GetJArray(); - - [Post("/form")] - Task FormPost([FormParam] string value); - - [Path("/headers")] - Task SendFooHeader([HeaderParam("X-Foo")] string foo); - - [Path("/bytes")] - Task GetByteArray(); - - [Path("/byte-object")] - Task GetByteArrayObject(); - - [Path("/string")] - [Accept("text/plain")] - Task GetString(); - - [Path("/string-object")] - [Accept("text/plain")] - Task GetStringObject(); - } - - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - public class StringObject - { - public string Value { get; } - - public StringObject(string value) - { - Value = value; - } - } - - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - public class ByteArrayObject - { - public byte[] Bytes { get; } - - public ByteArrayObject(byte[] bytes) - { - Bytes = bytes; - } } } \ No newline at end of file diff --git a/Activout.RestClient.Newtonsoft.Json.Test/RestClientTests.cs b/Activout.RestClient.Newtonsoft.Json.Test/RestClientTests.cs index ec19802..3dd1a10 100644 --- a/Activout.RestClient.Newtonsoft.Json.Test/RestClientTests.cs +++ b/Activout.RestClient.Newtonsoft.Json.Test/RestClientTests.cs @@ -2,14 +2,12 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using Activout.RestClient.Helpers.Implementation; using Activout.RestClient.Newtonsoft.Json.Test.MovieReviews; using Microsoft.Extensions.Logging; -using Moq; using Newtonsoft.Json; using RichardSzalay.MockHttp; using Xunit; @@ -149,105 +147,6 @@ public void TestErrorSync() Assert.Equal("Sorry, that page does not exist", error.Errors[0].Message); } - - [Fact] - public async Task TestTimeoutAsync() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies") - .Respond(async () => - { - await Task.Delay(1000); - return null; - }); - - var httpClient = _mockHttp.ToHttpClient(); - httpClient.Timeout = TimeSpan.FromMilliseconds(1); - var reviewSvc = _restClientFactory.CreateBuilder() - .With(httpClient) - .BaseUri(new Uri(BaseUri)) - .Build(); - - // act - await Assert.ThrowsAsync(() => reviewSvc.GetAllMovies()); - - // assert - } - - [Fact] - public async Task TestCancellationAsync() - { - // arrange - _mockHttp.When($"{BaseUri}/movies") - .Respond(_ => null); - - var reviewSvc = CreateMovieReviewService(); - var cancellationTokenSource = new CancellationTokenSource(); - - // act - cancellationTokenSource.Cancel(); - await Assert.ThrowsAsync(() => - reviewSvc.GetAllMoviesCancellable(cancellationTokenSource.Token)); - - // assert - } - - [Fact] - public async Task TestNoCancellationAsync() - { - // arrange - _mockHttp.When($"{BaseUri}/movies") - .Respond("application/json", "[]"); - - var reviewSvc = CreateMovieReviewService(); - - // act - var movies = await reviewSvc.GetAllMoviesCancellable(default); - - // assert - Assert.Empty(movies); - } - - [Fact] - public void TestErrorEmptyNoContentType() - { - // arrange - _mockHttp - .When(HttpMethod.Get, $"{BaseUri}/movies/fail") - .Respond(HttpStatusCode.BadRequest, request => new ByteArrayContent(new byte[0])); - - var reviewSvc = CreateMovieReviewService(); - - // act - var aggregateException = Assert.Throws(() => reviewSvc.Fail()); - - // assert - var exception = (RestClientException)aggregateException.GetBaseException(); - Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); - - Assert.NotNull(exception.ErrorResponse); - Assert.IsType(exception.ErrorResponse); - Assert.Empty(exception.GetErrorResponse()); - } - - [Fact] - public void TestDelete() - { - // arrange - _mockHttp - .Expect(HttpMethod.Delete, $"{BaseUri}/movies/{MovieId}/reviews/{ReviewId}") - .Respond(HttpStatusCode.OK); - - var reviewSvc = CreateMovieReviewService(); - - // act - reviewSvc.DeleteReview(MovieId, ReviewId); - - // assert - _mockHttp.VerifyNoOutstandingExpectation(); - } - [Fact] public async Task TestGetEmptyIEnumerableAsync() { @@ -317,25 +216,6 @@ public async Task TestPostJsonAsync() Assert.Equal(text, result.Text); } - [Fact] - public async Task TestPostTextAsync() - { - // arrange - _mockHttp - .When(HttpMethod.Post, $"{BaseUri}/movies/import.csv") - .WithContent("foobar") - .WithHeaders("Content-Type", "text/csv; charset=utf-8") - .Respond(HttpStatusCode.NoContent); - - var reviewSvc = CreateMovieReviewService(); - - // act - await reviewSvc.Import("foobar"); - - // assert - //Assert.Equal("*REVIEW_ID*", result.ReviewId); - } - [Fact] public void TestPutSync() { @@ -394,40 +274,6 @@ public void TestPatchSync() Assert.Equal(text, result.Text); } - [Fact] - public async Task TestGetHttpContent() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies") - .Respond("application/json", "[]"); - - var reviewSvc = CreateMovieReviewService(); - - // act - var httpContent = reviewSvc.GetHttpContent(); - - // assert - Assert.Equal("[]", await httpContent.ReadAsStringAsync()); - } - - [Fact] - public async Task TestGetHttpResponseMessage() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies") - .Respond("application/json", "[]"); - - var reviewSvc = CreateMovieReviewService(); - - // act - var httpResponseMessage = reviewSvc.GetHttpResponseMessage(); - - // assert - Assert.Equal("[]", await httpResponseMessage.Content.ReadAsStringAsync()); - } - [Fact] public void TestGetJObject() { @@ -463,159 +309,5 @@ public async Task TestGetJArray() string foo = jArray[0].foo; Assert.Equal("bar", foo); } - - [Fact] - public async Task TestGetByteArray() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/bytes") - .Respond(new ByteArrayContent(new byte[] { 42 })); - - var reviewSvc = CreateMovieReviewService(); - - // act - var bytes = await reviewSvc.GetByteArray(); - - // assert - Assert.Equal(new byte[] { 42 }, bytes); - } - - [Fact] - public async Task TestGetByteArrayObject() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/byte-object") - .Respond(new ByteArrayContent(new byte[] { 42 })); - - var reviewSvc = CreateMovieReviewService(); - - // act - var byteArrayObject = await reviewSvc.GetByteArrayObject(); - - // assert - Assert.Equal(new byte[] { 42 }, byteArrayObject.Bytes); - } - - [Fact] - public async Task TestGetString() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/string") - .WithHeaders("accept", "text/plain") - .Respond(new StringContent("foo")); - - var reviewSvc = CreateMovieReviewService(); - - // act - var result = await reviewSvc.GetString(); - - // assert - Assert.Equal("foo", result); - } - - [Fact] - public async Task TestGetStringObject() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/string-object") - .WithHeaders("accept", "text/plain") - .Respond(new StringContent("bar")); - - var reviewSvc = CreateMovieReviewService(); - - // act - var stringObject = await reviewSvc.GetStringObject(); - - // assert - Assert.Equal("bar", stringObject.Value); - } - - [Fact] - public async Task TestFormPost() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/form") - .WithFormData("value", "foobar") - .Respond("text/plain", ""); - - var reviewSvc = CreateMovieReviewService(); - - // act - await reviewSvc.FormPost("foobar"); - - // assert - } - - [Fact] - public async Task TestRequestLogger() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies") - .Respond("application/json", "[]"); - - var requestLoggerMock = new Mock(); - requestLoggerMock.Setup(x => x.TimeOperation(It.IsAny())) - .Returns(() => new Mock().Object); - - var reviewSvc = CreateRestClientBuilder() - .With(requestLoggerMock.Object) - .Build(); - - // act - await reviewSvc.GetAllMovies(); - await reviewSvc.GetAllMovies(); - - // assert - requestLoggerMock.Verify(x => x.TimeOperation(It.IsAny()), Times.Exactly(2)); - requestLoggerMock.VerifyNoOtherCalls(); - _mockHttp.VerifyNoOutstandingExpectation(); - } - - [Fact] - public async Task TestHeaderParam() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/headers") - .WithHeaders("X-Foo", "bar") - .Respond("text/plain", ""); - - var reviewSvc = CreateRestClientBuilder() - .Header(new AuthenticationHeaderValue("Basic", "SECRET")) - .Header("X-Tick", new TickValue()) - .Build(); - - // act - var responseMessage1 = await reviewSvc.SendFooHeader("bar"); - var requestHeaders1 = responseMessage1.RequestMessage.Headers; - - var responseMessage2 = await reviewSvc.SendFooHeader("bar"); - var requestHeaders2 = responseMessage2.RequestMessage.Headers; - - // assert - Assert.NotNull(requestHeaders1.Authorization); - Assert.Equal("Basic SECRET", requestHeaders1.Authorization.ToString()); - Assert.NotEmpty(requestHeaders1.GetValues("X-Tick")); - - Assert.NotEqual( - requestHeaders1.GetValues("X-Tick").First(), - requestHeaders2.GetValues("X-Tick").First()); - } - } - - internal class TickValue - { - private long _ticks = 42; - - public override string ToString() - { - return Interlocked.Increment(ref _ticks).ToString(); - } } } \ No newline at end of file diff --git a/Activout.RestClient.Test/MovieReviews/ErrorResponse.cs b/Activout.RestClient.Test/MovieReviews/ErrorResponse.cs new file mode 100644 index 0000000..3b3876f --- /dev/null +++ b/Activout.RestClient.Test/MovieReviews/ErrorResponse.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Activout.RestClient.Test.MovieReviews +{ + public class ErrorResponse + { + public List Errors { get; set; } + + public class Error + { + public string Message { get; set; } + public int Code { get; set; } + } + } +} \ No newline at end of file diff --git a/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs b/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs new file mode 100644 index 0000000..61bfd43 --- /dev/null +++ b/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs @@ -0,0 +1,68 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Activout.RestClient.Test.MovieReviews +{ + [Path("movies")] + [ErrorResponse(typeof(ErrorResponse))] + public interface IMovieReviewService + { + [Delete("/{movieId}/reviews/{reviewId}")] + void DeleteReview(string movieId, string reviewId); + + [Get("/fail")] + [ErrorResponse(typeof(byte[]))] + void Fail(); + + [Post("/import.csv")] + [ContentType("text/csv")] + Task Import(string csv); + + HttpContent GetHttpContent(); + + HttpResponseMessage GetHttpResponseMessage(); + + [Path("/bytes")] + Task GetByteArray(); + + [Path("/byte-object")] + Task GetByteArrayObject(); + + [Path("/string")] + [Accept("text/plain")] + Task GetString(); + + [Path("/string-object")] + [Accept("text/plain")] + Task GetStringObject(); + + [Post("/form")] + Task FormPost([FormParam] string value); + + [Path("/headers")] + Task SendFooHeader([HeaderParam("X-Foo")] string foo); + } + + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + public class StringObject + { + public string Value { get; } + + public StringObject(string value) + { + Value = value; + } + } + + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + public class ByteArrayObject + { + public byte[] Bytes { get; } + + public ByteArrayObject(byte[] bytes) + { + Bytes = bytes; + } + } +} \ No newline at end of file diff --git a/Activout.RestClient.Test/NonJsonRestClientTests.cs b/Activout.RestClient.Test/NonJsonRestClientTests.cs new file mode 100644 index 0000000..68d117b --- /dev/null +++ b/Activout.RestClient.Test/NonJsonRestClientTests.cs @@ -0,0 +1,267 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Activout.RestClient.Test.MovieReviews; +using Microsoft.Extensions.Logging; +using RichardSzalay.MockHttp; +using Xunit; +using Xunit.Abstractions; + +namespace Activout.RestClient.Test +{ + public class NonJsonRestClientTests + { + public NonJsonRestClientTests(ITestOutputHelper outputHelper) + { + _restClientFactory = Services.CreateRestClientFactory(); + _mockHttp = new MockHttpMessageHandler(); + _loggerFactory = LoggerFactoryHelpers.CreateLoggerFactory(outputHelper); + } + + private const string BaseUri = "https://example.com/movieReviewService"; + private const string MovieId = "*MOVIE_ID*"; + private const string ReviewId = "*REVIEW_ID*"; + + private readonly IRestClientFactory _restClientFactory; + private readonly MockHttpMessageHandler _mockHttp; + private readonly ILoggerFactory _loggerFactory; + + private IRestClientBuilder CreateRestClientBuilder() + { + return _restClientFactory.CreateBuilder() + .With(_loggerFactory.CreateLogger()) + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri); + } + + private IMovieReviewService CreateMovieReviewService() + { + return CreateRestClientBuilder() + .Build(); + } + + [Fact] + public void TestErrorEmptyNoContentType() + { + // arrange + _mockHttp + .When(HttpMethod.Get, $"{BaseUri}/movies/fail") + .Respond(HttpStatusCode.BadRequest, request => new ByteArrayContent(new byte[0])); + + var reviewSvc = CreateMovieReviewService(); + + // act + var aggregateException = Assert.Throws(() => reviewSvc.Fail()); + + // assert + var exception = (RestClientException)aggregateException.GetBaseException(); + Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); + + Assert.NotNull(exception.ErrorResponse); + Assert.IsType(exception.ErrorResponse); + Assert.Empty(exception.GetErrorResponse()); + } + + [Fact] + public void TestDelete() + { + // arrange + _mockHttp + .Expect(HttpMethod.Delete, $"{BaseUri}/movies/{MovieId}/reviews/{ReviewId}") + .Respond(HttpStatusCode.OK); + + var reviewSvc = CreateMovieReviewService(); + + // act + reviewSvc.DeleteReview(MovieId, ReviewId); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestPostTextAsync() + { + // arrange + _mockHttp + .When(HttpMethod.Post, $"{BaseUri}/movies/import.csv") + .WithContent("foobar") + .WithHeaders("Content-Type", "text/csv; charset=utf-8") + .Respond(HttpStatusCode.NoContent); + + var reviewSvc = CreateMovieReviewService(); + + // act + await reviewSvc.Import("foobar"); + + // assert + //Assert.Equal("*REVIEW_ID*", result.ReviewId); + } + + [Fact] + public async Task TestGetHttpContent() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies") + .Respond("text/plain", "test content"); + + var reviewSvc = CreateMovieReviewService(); + + // act + var httpContent = reviewSvc.GetHttpContent(); + + // assert + Assert.Equal("test content", await httpContent.ReadAsStringAsync()); + } + + [Fact] + public async Task TestGetHttpResponseMessage() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies") + .Respond("text/plain", "test content"); + + var reviewSvc = CreateMovieReviewService(); + + // act + var httpResponseMessage = reviewSvc.GetHttpResponseMessage(); + + // assert + Assert.Equal("test content", await httpResponseMessage.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task TestGetByteArray() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/bytes") + .Respond(new ByteArrayContent(new byte[] { 42 })); + + var reviewSvc = CreateMovieReviewService(); + + // act + var bytes = await reviewSvc.GetByteArray(); + + // assert + Assert.Equal(new byte[] { 42 }, bytes); + } + + [Fact] + public async Task TestGetByteArrayObject() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/byte-object") + .Respond(new ByteArrayContent(new byte[] { 42 })); + + var reviewSvc = CreateMovieReviewService(); + + // act + var byteArrayObject = await reviewSvc.GetByteArrayObject(); + + // assert + Assert.Equal(new byte[] { 42 }, byteArrayObject.Bytes); + } + + [Fact] + public async Task TestGetString() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/string") + .WithHeaders("accept", "text/plain") + .Respond(new StringContent("foo")); + + var reviewSvc = CreateMovieReviewService(); + + // act + var result = await reviewSvc.GetString(); + + // assert + Assert.Equal("foo", result); + } + + [Fact] + public async Task TestGetStringObject() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/string-object") + .WithHeaders("accept", "text/plain") + .Respond(new StringContent("bar")); + + var reviewSvc = CreateMovieReviewService(); + + // act + var stringObject = await reviewSvc.GetStringObject(); + + // assert + Assert.Equal("bar", stringObject.Value); + } + + [Fact] + public async Task TestFormPost() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/form") + .WithFormData("value", "foobar") + .Respond("text/plain", ""); + + var reviewSvc = CreateMovieReviewService(); + + // act + await reviewSvc.FormPost("foobar"); + + // assert + } + + [Fact] + public async Task TestHeaderParam() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/headers") + .WithHeaders("X-Foo", "bar") + .Respond("text/plain", ""); + + var reviewSvc = CreateRestClientBuilder() + .Header(new AuthenticationHeaderValue("Basic", "SECRET")) + .Header("X-Tick", new TickValue()) + .Build(); + + // act + var responseMessage1 = await reviewSvc.SendFooHeader("bar"); + var requestHeaders1 = responseMessage1.RequestMessage.Headers; + + var responseMessage2 = await reviewSvc.SendFooHeader("bar"); + var requestHeaders2 = responseMessage2.RequestMessage.Headers; + + // assert + Assert.NotNull(requestHeaders1.Authorization); + Assert.Equal("Basic SECRET", requestHeaders1.Authorization.ToString()); + Assert.NotEmpty(requestHeaders1.GetValues("X-Tick")); + + Assert.NotEqual( + requestHeaders1.GetValues("X-Tick").First(), + requestHeaders2.GetValues("X-Tick").First()); + } + } + + internal class TickValue + { + private long _ticks = 42; + + public override string ToString() + { + return Interlocked.Increment(ref _ticks).ToString(); + } + } +} \ No newline at end of file From fc7bb5284d082cd7fb130295662a7d4d54f27e98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:19:16 +0000 Subject: [PATCH 3/5] Use file-based namespaces and primary constructors in newly created files Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- .../MovieReviews/ErrorResponse.cs | 19 +- .../MovieReviews/IMovieReviewService.cs | 89 ++-- .../NonJsonRestClientTests.cs | 484 +++++++++--------- 3 files changed, 286 insertions(+), 306 deletions(-) diff --git a/Activout.RestClient.Test/MovieReviews/ErrorResponse.cs b/Activout.RestClient.Test/MovieReviews/ErrorResponse.cs index 3b3876f..6fb09f3 100644 --- a/Activout.RestClient.Test/MovieReviews/ErrorResponse.cs +++ b/Activout.RestClient.Test/MovieReviews/ErrorResponse.cs @@ -1,15 +1,14 @@ using System.Collections.Generic; -namespace Activout.RestClient.Test.MovieReviews +namespace Activout.RestClient.Test.MovieReviews; + +public class ErrorResponse { - public class ErrorResponse - { - public List Errors { get; set; } + public List Errors { get; set; } - public class Error - { - public string Message { get; set; } - public int Code { get; set; } - } + public class Error + { + public string Message { get; set; } + public int Code { get; set; } } -} \ No newline at end of file +} diff --git a/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs b/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs index 61bfd43..8a735a6 100644 --- a/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs +++ b/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs @@ -2,67 +2,56 @@ using System.Net.Http; using System.Threading.Tasks; -namespace Activout.RestClient.Test.MovieReviews -{ - [Path("movies")] - [ErrorResponse(typeof(ErrorResponse))] - public interface IMovieReviewService - { - [Delete("/{movieId}/reviews/{reviewId}")] - void DeleteReview(string movieId, string reviewId); - - [Get("/fail")] - [ErrorResponse(typeof(byte[]))] - void Fail(); +namespace Activout.RestClient.Test.MovieReviews; - [Post("/import.csv")] - [ContentType("text/csv")] - Task Import(string csv); +[Path("movies")] +[ErrorResponse(typeof(ErrorResponse))] +public interface IMovieReviewService +{ + [Delete("/{movieId}/reviews/{reviewId}")] + void DeleteReview(string movieId, string reviewId); - HttpContent GetHttpContent(); + [Get("/fail")] + [ErrorResponse(typeof(byte[]))] + void Fail(); - HttpResponseMessage GetHttpResponseMessage(); + [Post("/import.csv")] + [ContentType("text/csv")] + Task Import(string csv); - [Path("/bytes")] - Task GetByteArray(); + HttpContent GetHttpContent(); - [Path("/byte-object")] - Task GetByteArrayObject(); + HttpResponseMessage GetHttpResponseMessage(); - [Path("/string")] - [Accept("text/plain")] - Task GetString(); + [Path("/bytes")] + Task GetByteArray(); - [Path("/string-object")] - [Accept("text/plain")] - Task GetStringObject(); + [Path("/byte-object")] + Task GetByteArrayObject(); - [Post("/form")] - Task FormPost([FormParam] string value); + [Path("/string")] + [Accept("text/plain")] + Task GetString(); - [Path("/headers")] - Task SendFooHeader([HeaderParam("X-Foo")] string foo); - } + [Path("/string-object")] + [Accept("text/plain")] + Task GetStringObject(); - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - public class StringObject - { - public string Value { get; } + [Post("/form")] + Task FormPost([FormParam] string value); - public StringObject(string value) - { - Value = value; - } - } + [Path("/headers")] + Task SendFooHeader([HeaderParam("X-Foo")] string foo); +} - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - public class ByteArrayObject - { - public byte[] Bytes { get; } +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +public class StringObject(string value) +{ + public string Value { get; } = value; +} - public ByteArrayObject(byte[] bytes) - { - Bytes = bytes; - } - } +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +public class ByteArrayObject(byte[] bytes) +{ + public byte[] Bytes { get; } = bytes; } \ No newline at end of file diff --git a/Activout.RestClient.Test/NonJsonRestClientTests.cs b/Activout.RestClient.Test/NonJsonRestClientTests.cs index 68d117b..fdd05a7 100644 --- a/Activout.RestClient.Test/NonJsonRestClientTests.cs +++ b/Activout.RestClient.Test/NonJsonRestClientTests.cs @@ -11,257 +11,249 @@ using Xunit; using Xunit.Abstractions; -namespace Activout.RestClient.Test +namespace Activout.RestClient.Test; + +public class NonJsonRestClientTests(ITestOutputHelper outputHelper) { - public class NonJsonRestClientTests + private const string BaseUri = "https://example.com/movieReviewService"; + private const string MovieId = "*MOVIE_ID*"; + private const string ReviewId = "*REVIEW_ID*"; + + private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory(); + private readonly MockHttpMessageHandler _mockHttp = new MockHttpMessageHandler(); + private readonly ILoggerFactory _loggerFactory = LoggerFactoryHelpers.CreateLoggerFactory(outputHelper); + + private IRestClientBuilder CreateRestClientBuilder() + { + return _restClientFactory.CreateBuilder() + .With(_loggerFactory.CreateLogger()) + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri); + } + + private IMovieReviewService CreateMovieReviewService() + { + return CreateRestClientBuilder() + .Build(); + } + + [Fact] + public void TestErrorEmptyNoContentType() + { + // arrange + _mockHttp + .When(HttpMethod.Get, $"{BaseUri}/movies/fail") + .Respond(HttpStatusCode.BadRequest, request => new ByteArrayContent(new byte[0])); + + var reviewSvc = CreateMovieReviewService(); + + // act + var aggregateException = Assert.Throws(() => reviewSvc.Fail()); + + // assert + var exception = (RestClientException)aggregateException.GetBaseException(); + Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); + + Assert.NotNull(exception.ErrorResponse); + Assert.IsType(exception.ErrorResponse); + Assert.Empty(exception.GetErrorResponse()); + } + + [Fact] + public void TestDelete() + { + // arrange + _mockHttp + .Expect(HttpMethod.Delete, $"{BaseUri}/movies/{MovieId}/reviews/{ReviewId}") + .Respond(HttpStatusCode.OK); + + var reviewSvc = CreateMovieReviewService(); + + // act + reviewSvc.DeleteReview(MovieId, ReviewId); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestPostTextAsync() + { + // arrange + _mockHttp + .When(HttpMethod.Post, $"{BaseUri}/movies/import.csv") + .WithContent("foobar") + .WithHeaders("Content-Type", "text/csv; charset=utf-8") + .Respond(HttpStatusCode.NoContent); + + var reviewSvc = CreateMovieReviewService(); + + // act + await reviewSvc.Import("foobar"); + + // assert + //Assert.Equal("*REVIEW_ID*", result.ReviewId); + } + + [Fact] + public async Task TestGetHttpContent() { - public NonJsonRestClientTests(ITestOutputHelper outputHelper) - { - _restClientFactory = Services.CreateRestClientFactory(); - _mockHttp = new MockHttpMessageHandler(); - _loggerFactory = LoggerFactoryHelpers.CreateLoggerFactory(outputHelper); - } - - private const string BaseUri = "https://example.com/movieReviewService"; - private const string MovieId = "*MOVIE_ID*"; - private const string ReviewId = "*REVIEW_ID*"; - - private readonly IRestClientFactory _restClientFactory; - private readonly MockHttpMessageHandler _mockHttp; - private readonly ILoggerFactory _loggerFactory; - - private IRestClientBuilder CreateRestClientBuilder() - { - return _restClientFactory.CreateBuilder() - .With(_loggerFactory.CreateLogger()) - .With(_mockHttp.ToHttpClient()) - .BaseUri(BaseUri); - } - - private IMovieReviewService CreateMovieReviewService() - { - return CreateRestClientBuilder() - .Build(); - } - - [Fact] - public void TestErrorEmptyNoContentType() - { - // arrange - _mockHttp - .When(HttpMethod.Get, $"{BaseUri}/movies/fail") - .Respond(HttpStatusCode.BadRequest, request => new ByteArrayContent(new byte[0])); - - var reviewSvc = CreateMovieReviewService(); - - // act - var aggregateException = Assert.Throws(() => reviewSvc.Fail()); - - // assert - var exception = (RestClientException)aggregateException.GetBaseException(); - Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); - - Assert.NotNull(exception.ErrorResponse); - Assert.IsType(exception.ErrorResponse); - Assert.Empty(exception.GetErrorResponse()); - } - - [Fact] - public void TestDelete() - { - // arrange - _mockHttp - .Expect(HttpMethod.Delete, $"{BaseUri}/movies/{MovieId}/reviews/{ReviewId}") - .Respond(HttpStatusCode.OK); - - var reviewSvc = CreateMovieReviewService(); - - // act - reviewSvc.DeleteReview(MovieId, ReviewId); - - // assert - _mockHttp.VerifyNoOutstandingExpectation(); - } - - [Fact] - public async Task TestPostTextAsync() - { - // arrange - _mockHttp - .When(HttpMethod.Post, $"{BaseUri}/movies/import.csv") - .WithContent("foobar") - .WithHeaders("Content-Type", "text/csv; charset=utf-8") - .Respond(HttpStatusCode.NoContent); - - var reviewSvc = CreateMovieReviewService(); - - // act - await reviewSvc.Import("foobar"); - - // assert - //Assert.Equal("*REVIEW_ID*", result.ReviewId); - } - - [Fact] - public async Task TestGetHttpContent() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies") - .Respond("text/plain", "test content"); - - var reviewSvc = CreateMovieReviewService(); - - // act - var httpContent = reviewSvc.GetHttpContent(); - - // assert - Assert.Equal("test content", await httpContent.ReadAsStringAsync()); - } - - [Fact] - public async Task TestGetHttpResponseMessage() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies") - .Respond("text/plain", "test content"); - - var reviewSvc = CreateMovieReviewService(); - - // act - var httpResponseMessage = reviewSvc.GetHttpResponseMessage(); - - // assert - Assert.Equal("test content", await httpResponseMessage.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task TestGetByteArray() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/bytes") - .Respond(new ByteArrayContent(new byte[] { 42 })); - - var reviewSvc = CreateMovieReviewService(); - - // act - var bytes = await reviewSvc.GetByteArray(); - - // assert - Assert.Equal(new byte[] { 42 }, bytes); - } - - [Fact] - public async Task TestGetByteArrayObject() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/byte-object") - .Respond(new ByteArrayContent(new byte[] { 42 })); - - var reviewSvc = CreateMovieReviewService(); - - // act - var byteArrayObject = await reviewSvc.GetByteArrayObject(); - - // assert - Assert.Equal(new byte[] { 42 }, byteArrayObject.Bytes); - } - - [Fact] - public async Task TestGetString() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/string") - .WithHeaders("accept", "text/plain") - .Respond(new StringContent("foo")); - - var reviewSvc = CreateMovieReviewService(); - - // act - var result = await reviewSvc.GetString(); - - // assert - Assert.Equal("foo", result); - } - - [Fact] - public async Task TestGetStringObject() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/string-object") - .WithHeaders("accept", "text/plain") - .Respond(new StringContent("bar")); - - var reviewSvc = CreateMovieReviewService(); - - // act - var stringObject = await reviewSvc.GetStringObject(); - - // assert - Assert.Equal("bar", stringObject.Value); - } - - [Fact] - public async Task TestFormPost() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/form") - .WithFormData("value", "foobar") - .Respond("text/plain", ""); - - var reviewSvc = CreateMovieReviewService(); - - // act - await reviewSvc.FormPost("foobar"); - - // assert - } - - [Fact] - public async Task TestHeaderParam() - { - // arrange - _mockHttp - .When($"{BaseUri}/movies/headers") - .WithHeaders("X-Foo", "bar") - .Respond("text/plain", ""); - - var reviewSvc = CreateRestClientBuilder() - .Header(new AuthenticationHeaderValue("Basic", "SECRET")) - .Header("X-Tick", new TickValue()) - .Build(); - - // act - var responseMessage1 = await reviewSvc.SendFooHeader("bar"); - var requestHeaders1 = responseMessage1.RequestMessage.Headers; - - var responseMessage2 = await reviewSvc.SendFooHeader("bar"); - var requestHeaders2 = responseMessage2.RequestMessage.Headers; - - // assert - Assert.NotNull(requestHeaders1.Authorization); - Assert.Equal("Basic SECRET", requestHeaders1.Authorization.ToString()); - Assert.NotEmpty(requestHeaders1.GetValues("X-Tick")); - - Assert.NotEqual( - requestHeaders1.GetValues("X-Tick").First(), - requestHeaders2.GetValues("X-Tick").First()); - } + // arrange + _mockHttp + .When($"{BaseUri}/movies") + .Respond("text/plain", "test content"); + + var reviewSvc = CreateMovieReviewService(); + + // act + var httpContent = reviewSvc.GetHttpContent(); + + // assert + Assert.Equal("test content", await httpContent.ReadAsStringAsync()); } - internal class TickValue + [Fact] + public async Task TestGetHttpResponseMessage() { - private long _ticks = 42; + // arrange + _mockHttp + .When($"{BaseUri}/movies") + .Respond("text/plain", "test content"); + + var reviewSvc = CreateMovieReviewService(); - public override string ToString() - { - return Interlocked.Increment(ref _ticks).ToString(); - } + // act + var httpResponseMessage = reviewSvc.GetHttpResponseMessage(); + + // assert + Assert.Equal("test content", await httpResponseMessage.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task TestGetByteArray() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/bytes") + .Respond(new ByteArrayContent(new byte[] { 42 })); + + var reviewSvc = CreateMovieReviewService(); + + // act + var bytes = await reviewSvc.GetByteArray(); + + // assert + Assert.Equal(new byte[] { 42 }, bytes); + } + + [Fact] + public async Task TestGetByteArrayObject() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/byte-object") + .Respond(new ByteArrayContent(new byte[] { 42 })); + + var reviewSvc = CreateMovieReviewService(); + + // act + var byteArrayObject = await reviewSvc.GetByteArrayObject(); + + // assert + Assert.Equal(new byte[] { 42 }, byteArrayObject.Bytes); + } + + [Fact] + public async Task TestGetString() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/string") + .WithHeaders("accept", "text/plain") + .Respond(new StringContent("foo")); + + var reviewSvc = CreateMovieReviewService(); + + // act + var result = await reviewSvc.GetString(); + + // assert + Assert.Equal("foo", result); + } + + [Fact] + public async Task TestGetStringObject() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/string-object") + .WithHeaders("accept", "text/plain") + .Respond(new StringContent("bar")); + + var reviewSvc = CreateMovieReviewService(); + + // act + var stringObject = await reviewSvc.GetStringObject(); + + // assert + Assert.Equal("bar", stringObject.Value); + } + + [Fact] + public async Task TestFormPost() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/form") + .WithFormData("value", "foobar") + .Respond("text/plain", ""); + + var reviewSvc = CreateMovieReviewService(); + + // act + await reviewSvc.FormPost("foobar"); + + // assert + } + + [Fact] + public async Task TestHeaderParam() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/headers") + .WithHeaders("X-Foo", "bar") + .Respond("text/plain", ""); + + var reviewSvc = CreateRestClientBuilder() + .Header(new AuthenticationHeaderValue("Basic", "SECRET")) + .Header("X-Tick", new TickValue()) + .Build(); + + // act + var responseMessage1 = await reviewSvc.SendFooHeader("bar"); + var requestHeaders1 = responseMessage1.RequestMessage.Headers; + + var responseMessage2 = await reviewSvc.SendFooHeader("bar"); + var requestHeaders2 = responseMessage2.RequestMessage.Headers; + + // assert + Assert.NotNull(requestHeaders1.Authorization); + Assert.Equal("Basic SECRET", requestHeaders1.Authorization.ToString()); + Assert.NotEmpty(requestHeaders1.GetValues("X-Tick")); + + Assert.NotEqual( + requestHeaders1.GetValues("X-Tick").First(), + requestHeaders2.GetValues("X-Tick").First()); + } +} + +internal class TickValue +{ + private long _ticks = 42; + + public override string ToString() + { + return Interlocked.Increment(ref _ticks).ToString(); } } \ No newline at end of file From 2544ca9a3eb997b62929556406fd8002031d2bbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:35:11 +0000 Subject: [PATCH 4/5] Move remaining 4 tests to core project with non-JSON methods Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- .../MovieReviews/IMovieReviewService.cs | 8 ++ .../NonJsonRestClientTests.cs | 84 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs b/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs index 8a735a6..a2a672a 100644 --- a/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs +++ b/Activout.RestClient.Test/MovieReviews/IMovieReviewService.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace Activout.RestClient.Test.MovieReviews; @@ -42,6 +43,13 @@ public interface IMovieReviewService [Path("/headers")] Task SendFooHeader([HeaderParam("X-Foo")] string foo); + + [Path("/string")] + [Accept("text/plain")] + Task GetStringCancellable(CancellationToken cancellationToken); + + [Path("/bytes")] + Task GetByteArrayCancellable(CancellationToken cancellationToken); } [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] diff --git a/Activout.RestClient.Test/NonJsonRestClientTests.cs b/Activout.RestClient.Test/NonJsonRestClientTests.cs index fdd05a7..08c6955 100644 --- a/Activout.RestClient.Test/NonJsonRestClientTests.cs +++ b/Activout.RestClient.Test/NonJsonRestClientTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Activout.RestClient.Test.MovieReviews; using Microsoft.Extensions.Logging; +using Moq; using RichardSzalay.MockHttp; using Xunit; using Xunit.Abstractions; @@ -37,6 +38,89 @@ private IMovieReviewService CreateMovieReviewService() .Build(); } + [Fact] + public async Task TestTimeoutAsync() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/string") + .Respond(async () => + { + await Task.Delay(1000); + return null; + }); + + var httpClient = _mockHttp.ToHttpClient(); + httpClient.Timeout = TimeSpan.FromMilliseconds(1); + var reviewSvc = _restClientFactory.CreateBuilder() + .With(httpClient) + .BaseUri(new Uri(BaseUri)) + .Build(); + + // act + await Assert.ThrowsAsync(() => reviewSvc.GetString()); + + // assert + } + + [Fact] + public async Task TestCancellationAsync() + { + // arrange + _mockHttp.When($"{BaseUri}/movies/string") + .Respond(_ => null); + + var reviewSvc = CreateMovieReviewService(); + var cancellationTokenSource = new CancellationTokenSource(); + + // act + cancellationTokenSource.Cancel(); + await Assert.ThrowsAsync(() => + reviewSvc.GetStringCancellable(cancellationTokenSource.Token)); + + // assert + } + + [Fact] + public async Task TestNoCancellationAsync() + { + // arrange + _mockHttp.When($"{BaseUri}/movies/string") + .Respond("text/plain", "test string"); + + var reviewSvc = CreateMovieReviewService(); + + // act + var result = await reviewSvc.GetStringCancellable(default); + + // assert + Assert.Equal("test string", result); + } + + [Fact] + public async Task TestRequestLogger() + { + // arrange + _mockHttp + .When($"{BaseUri}/movies/string") + .Respond("text/plain", "test"); + + var requestLoggerMock = new Mock(); + requestLoggerMock.Setup(x => x.TimeOperation(It.IsAny())) + .Returns(() => new Mock().Object); + + var reviewSvc = CreateRestClientBuilder() + .With(requestLoggerMock.Object) + .Build(); + + // act + await reviewSvc.GetString(); + await reviewSvc.GetString(); + + // assert + requestLoggerMock.Verify(x => x.TimeOperation(It.IsAny()), Times.Exactly(2)); + } + [Fact] public void TestErrorEmptyNoContentType() { From f169daea5c116799e80d320427c84817df40f926 Mon Sep 17 00:00:00 2001 From: David Eriksson Date: Mon, 28 Jul 2025 22:04:05 +0200 Subject: [PATCH 5/5] Remove commented code Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: David Eriksson --- Activout.RestClient.Test/NonJsonRestClientTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Activout.RestClient.Test/NonJsonRestClientTests.cs b/Activout.RestClient.Test/NonJsonRestClientTests.cs index 08c6955..cd8f49e 100644 --- a/Activout.RestClient.Test/NonJsonRestClientTests.cs +++ b/Activout.RestClient.Test/NonJsonRestClientTests.cs @@ -176,7 +176,6 @@ public async Task TestPostTextAsync() await reviewSvc.Import("foobar"); // assert - //Assert.Equal("*REVIEW_ID*", result.ReviewId); } [Fact]