From c0c0dbe29b5bb597d35e1c4957fd202278478528 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:15:29 +0000 Subject: [PATCH 1/4] Initial plan From ad7496a98c45118a9cc1ec310cd35573860b950d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:19:53 +0000 Subject: [PATCH 2/4] Add comprehensive instructions.md for users Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- instructions.md | 617 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 617 insertions(+) create mode 100644 instructions.md diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..8b9f7af --- /dev/null +++ b/instructions.md @@ -0,0 +1,617 @@ +# Using Activout.RestClient in Your Project + +This guide shows you how to use Activout.RestClient to create type-safe REST API clients by defining C# interfaces, and how to unit test them using MockHttp. + +## Installation + +Install the required packages from NuGet: + +```bash +# Core package +dotnet add package Activout.RestClient + +# Choose one JSON serialization package: +dotnet add package Activout.RestClient.Json # For System.Text.Json +dotnet add package Activout.RestClient.Newtonsoft.Json # For Newtonsoft.Json + +# For unit testing with MockHttp +dotnet add package RichardSzalay.MockHttp --version 7.0.0 +dotnet add package xunit +``` + +## Step 1: Define Your API Interface + +Create an interface that describes your REST API endpoints. Use attributes to specify HTTP methods, paths, and parameters. + +```csharp +using Activout.RestClient; + +[Path("api/movies")] +[ErrorResponse(typeof(ErrorResponse))] +[Accept("application/json")] +[ContentType("application/json")] +public interface IMovieReviewService +{ + // GET /api/movies + [Get] + Task> GetAllMovies(); + + // GET /api/movies/{movieId}/reviews + [Get("/{movieId}/reviews")] + Task> GetAllReviews(string movieId); + + // GET /api/movies/{movieId}/reviews/{reviewId} + [Get("/{movieId}/reviews/{reviewId}")] + Task GetReview(string movieId, string reviewId); + + // POST /api/movies/{movieId}/reviews + [Post("/{movieId}/reviews")] + Task SubmitReview(string movieId, Review review); + + // PUT /api/movies/{movieId}/reviews/{reviewId} + [Put("/{movieId}/reviews/{reviewId}")] + Task UpdateReview(string movieId, string reviewId, Review review); + + // DELETE /api/movies/{movieId}/reviews/{reviewId} + [Delete("/{movieId}/reviews/{reviewId}")] + Task DeleteReview(string movieId, string reviewId); + + // GET /api/movies?begin=...&end=... + [Get] + Task> QueryMoviesByDate( + [QueryParam] DateTime begin, + [QueryParam] DateTime end); +} +``` + +### Available Attributes + +**Interface-level attributes:** +- `[Path("base/path")]` - Base path for all methods in the interface +- `[Accept("application/json")]` - Default Accept header +- `[ContentType("application/json")]` - Default Content-Type for POST/PUT requests +- `[ErrorResponse(typeof(ErrorResponse))]` - Type used to deserialize error responses + +**Method-level attributes:** +- `[Get]`, `[Post]`, `[Put]`, `[Delete]`, `[Patch]` - HTTP method +- `[Path("relative/path")]` - Relative path for the method + +**Parameter attributes:** +- `[PathParam]` - Path parameter (in URL path with `{paramName}`) +- `[QueryParam]` - Query string parameter (`?key=value`) +- `[HeaderParam]` - HTTP header parameter +- `[FormParam]` - Form data parameter (for `application/x-www-form-urlencoded`) +- `[PartParam]` - Multipart form data parameter + +## Step 2: Define Data Models + +Define your request and response models as regular C# classes or records: + +```csharp +public class Movie +{ + public string? Title { get; init; } + public int? Year { get; init; } +} + +public class Review +{ + public Review(int stars, string text) + { + Stars = stars; + Text = text; + } + + public string? MovieId { get; init; } + public string? ReviewId { get; init; } + public int Stars { get; init; } + public string Text { get; init; } +} + +public class ErrorResponse +{ + public List? Errors { get; init; } +} + +public class ErrorDetail +{ + public int Code { get; init; } + public string? Message { get; init; } +} +``` + +## Step 3: Create and Use the REST Client + +### Using System.Text.Json + +```csharp +using Activout.RestClient; +using Activout.RestClient.Json; + +var restClientFactory = new RestClientFactory(); +var movieService = restClientFactory + .CreateBuilder() + .With(httpClient) // Use your HttpClient + .WithSystemTextJson() // Enable System.Text.Json serialization + .BaseUri(new Uri("https://api.example.com")) + .Build(); + +// Use the client +var movies = await movieService.GetAllMovies(); +var review = new Review(5, "Amazing movie!"); +await movieService.SubmitReview("movie-123", review); +``` + +### Using Newtonsoft.Json + +```csharp +using Activout.RestClient; +using Activout.RestClient.Newtonsoft.Json; + +var restClientFactory = new RestClientFactory(); +var movieService = restClientFactory + .CreateBuilder() + .With(httpClient) + .WithNewtonsoftJson() // Enable Newtonsoft.Json serialization + .BaseUri(new Uri("https://api.example.com")) + .Build(); + +var movies = await movieService.GetAllMovies(); +``` + +### Using with Dependency Injection + +Add the required services to your `IServiceCollection`: + +```csharp +using Activout.RestClient; +using Activout.RestClient.Helpers.Implementation; +using Activout.RestClient.ParamConverter; +using Microsoft.Extensions.DependencyInjection; + +public static IServiceCollection AddRestClient(this IServiceCollection services) +{ + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + return services; +} + +// In your Startup.cs or Program.cs +services.AddRestClient(); +services.AddHttpClient(); + +// Register your specific API client +services.AddSingleton(provider => +{ + var httpClient = provider.GetRequiredService().CreateClient(); + var restClientFactory = provider.GetRequiredService(); + + return restClientFactory.CreateBuilder() + .With(httpClient) + .WithSystemTextJson() + .BaseUri(new Uri("https://api.example.com")) + .Build(); +}); +``` + +## Step 4: Unit Testing with MockHttp + +Use `RichardSzalay.MockHttp` to mock HTTP responses in your unit tests without making real HTTP calls. + +### Basic Test Setup + +```csharp +using Activout.RestClient; +using Activout.RestClient.Newtonsoft.Json; +using RichardSzalay.MockHttp; +using Xunit; + +public class MovieReviewServiceTests +{ + private const string BaseUri = "https://api.example.com"; + private readonly IRestClientFactory _restClientFactory; + private readonly MockHttpMessageHandler _mockHttp; + + public MovieReviewServiceTests() + { + _restClientFactory = new RestClientFactory(); + _mockHttp = new MockHttpMessageHandler(); + } + + private IMovieReviewService CreateMovieReviewService() + { + return _restClientFactory.CreateBuilder() + .WithNewtonsoftJson() + .With(_mockHttp.ToHttpClient()) // Use mock HTTP client + .BaseUri(BaseUri) + .Build(); + } +} +``` + +### Testing GET Requests + +```csharp +[Fact] +public async Task GetAllMovies_ReturnsMovieList() +{ + // Arrange + _mockHttp + .When($"{BaseUri}/api/movies") + .WithHeaders("Accept", "application/json") + .Respond("application/json", "[{\"Title\":\"Inception\",\"Year\":2010}]"); + + var service = CreateMovieReviewService(); + + // Act + var movies = await service.GetAllMovies(); + + // Assert + var movieList = movies.ToList(); + Assert.Single(movieList); + Assert.Equal("Inception", movieList[0].Title); + Assert.Equal(2010, movieList[0].Year); +} + +[Fact] +public async Task GetAllMovies_ReturnsEmptyList_WhenNoMovies() +{ + // Arrange + _mockHttp + .When($"{BaseUri}/api/movies") + .Respond("application/json", "[]"); + + var service = CreateMovieReviewService(); + + // Act + var movies = await service.GetAllMovies(); + + // Assert + Assert.Empty(movies); +} +``` + +### Testing POST Requests + +```csharp +[Fact] +public async Task SubmitReview_CreatesNewReview() +{ + // Arrange + var movieId = "movie-123"; + _mockHttp + .When(HttpMethod.Post, $"{BaseUri}/api/movies/{movieId}/reviews") + .WithHeaders("Content-Type", "application/json; charset=utf-8") + .Respond(request => + { + // Echo back the request with an added ReviewId + var content = request.Content!.ReadAsStringAsync().Result; + content = content.Replace("{", "{\"ReviewId\":\"review-456\", "); + return new StringContent(content, Encoding.UTF8, "application/json"); + }); + + var service = CreateMovieReviewService(); + var review = new Review(5, "Excellent movie!"); + + // Act + var result = await service.SubmitReview(movieId, review); + + // Assert + Assert.Equal("review-456", result.ReviewId); + Assert.Equal(5, result.Stars); + Assert.Equal("Excellent movie!", result.Text); +} +``` + +### Testing PUT Requests + +```csharp +[Fact] +public async Task UpdateReview_ModifiesExistingReview() +{ + // Arrange + var movieId = "movie-123"; + var reviewId = "review-456"; + _mockHttp + .When(HttpMethod.Put, $"{BaseUri}/api/movies/{movieId}/reviews/{reviewId}") + .Respond(request => request.Content!); // Echo back the request + + var service = CreateMovieReviewService(); + var updatedReview = new Review(4, "Good, but not great"); + + // Act + var result = await service.UpdateReview(movieId, reviewId, updatedReview); + + // Assert + Assert.Equal(4, result.Stars); + Assert.Equal("Good, but not great", result.Text); +} +``` + +### Testing Query Parameters + +```csharp +[Fact] +public async Task QueryMoviesByDate_UsesQueryParameters() +{ + // Arrange + var begin = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 12, 31, 0, 0, 0, DateTimeKind.Utc); + + _mockHttp + .When($"{BaseUri}/api/movies?begin=2020-01-01T00%3A00%3A00.0000000Z&end=2020-12-31T00%3A00%3A00.0000000Z") + .Respond("application/json", "[{\"Title\":\"Tenet\",\"Year\":2020}]"); + + var service = CreateMovieReviewService(); + + // Act + var movies = await service.QueryMoviesByDate(begin, end); + + // Assert + var movieList = movies.ToList(); + Assert.Single(movieList); + Assert.Equal("Tenet", movieList[0].Title); +} +``` + +### Testing Error Responses + +```csharp +[Fact] +public async Task GetReview_ThrowsRestClientException_WhenNotFound() +{ + // Arrange + var movieId = "movie-123"; + var reviewId = "invalid-review"; + + _mockHttp + .Expect(HttpMethod.Get, $"{BaseUri}/api/movies/{movieId}/reviews/{reviewId}") + .Respond(HttpStatusCode.NotFound, _ => new StringContent( + JsonConvert.SerializeObject(new + { + Errors = new[] + { + new { Code = 404, Message = "Review not found" } + } + }), + Encoding.UTF8, + "application/json")); + + var service = CreateMovieReviewService(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => service.GetReview(movieId, reviewId)); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + + var error = exception.GetErrorResponse(); + Assert.NotNull(error); + Assert.Equal(404, error.Errors![0].Code); + Assert.Equal("Review not found", error.Errors[0].Message); + + // Verify the expected call was made + _mockHttp.VerifyNoOutstandingExpectation(); +} +``` + +### Using Expect vs When + +MockHttp provides two ways to set up expectations: + +**`When()`** - Matches any number of requests: +```csharp +_mockHttp.When($"{BaseUri}/api/movies") + .Respond("application/json", "[]"); +``` + +**`Expect()`** - Expects exactly one request: +```csharp +_mockHttp.Expect($"{BaseUri}/api/movies") + .Respond("application/json", "[]"); + +// Verify all expectations were met +_mockHttp.VerifyNoOutstandingExpectation(); +``` + +### Testing with Custom JSON Settings + +For System.Text.Json: +```csharp +using System.Text.Json; +using System.Text.Json.Serialization; +using Activout.RestClient.Json; + +var jsonOptions = new JsonSerializerOptions(SystemTextJsonDefaults.SerializerOptions) +{ + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull +}; + +var service = _restClientFactory.CreateBuilder() + .WithSystemTextJson(jsonOptions) + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri) + .Build(); +``` + +For Newtonsoft.Json: +```csharp +using Newtonsoft.Json; +using Activout.RestClient.Newtonsoft.Json; + +var jsonSettings = new JsonSerializerSettings(NewtonsoftJsonDefaults.DefaultJsonSerializerSettings) +{ + NullValueHandling = NullValueHandling.Ignore +}; + +var service = _restClientFactory.CreateBuilder() + .WithNewtonsoftJson(jsonSettings) + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri) + .Build(); +``` + +## Complete Test Example + +Here's a complete test class demonstrating various scenarios: + +```csharp +using System.Net; +using System.Text; +using Activout.RestClient; +using Activout.RestClient.Newtonsoft.Json; +using Newtonsoft.Json; +using RichardSzalay.MockHttp; +using Xunit; + +namespace MyProject.Tests; + +public class MovieReviewServiceTests +{ + private const string BaseUri = "https://api.example.com"; + private readonly IRestClientFactory _restClientFactory; + private readonly MockHttpMessageHandler _mockHttp; + + public MovieReviewServiceTests() + { + _restClientFactory = new RestClientFactory(); + _mockHttp = new MockHttpMessageHandler(); + } + + private IMovieReviewService CreateService() + { + return _restClientFactory.CreateBuilder() + .WithNewtonsoftJson() + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri) + .Build(); + } + + [Fact] + public async Task GetAllMovies_Success() + { + _mockHttp + .When($"{BaseUri}/api/movies") + .Respond("application/json", "[{\"Title\":\"Inception\"}]"); + + var service = CreateService(); + var movies = await service.GetAllMovies(); + + Assert.Single(movies); + } + + [Fact] + public async Task SubmitReview_Success() + { + _mockHttp + .When(HttpMethod.Post, $"{BaseUri}/api/movies/123/reviews") + .Respond(request => + { + var content = request.Content!.ReadAsStringAsync().Result; + content = content.Replace("{", "{\"ReviewId\":\"456\", "); + return new StringContent(content, Encoding.UTF8, "application/json"); + }); + + var service = CreateService(); + var review = new Review(5, "Great!"); + var result = await service.SubmitReview("123", review); + + Assert.Equal("456", result.ReviewId); + Assert.Equal(5, result.Stars); + } + + [Fact] + public async Task GetReview_NotFound_ThrowsException() + { + _mockHttp + .When(HttpMethod.Get, $"{BaseUri}/api/movies/123/reviews/999") + .Respond(HttpStatusCode.NotFound, _ => new StringContent( + JsonConvert.SerializeObject(new + { + Errors = new[] { new { Code = 404, Message = "Not found" } } + }), + Encoding.UTF8, + "application/json")); + + var service = CreateService(); + + var exception = await Assert.ThrowsAsync( + () => service.GetReview("123", "999")); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + var error = exception.GetErrorResponse(); + Assert.Equal(404, error!.Errors![0].Code); + } +} +``` + +## Advanced Features + +### Custom Headers + +```csharp +[Get("/secure-endpoint")] +[Header("Authorization", "Bearer {token}")] +Task GetSecureData(string token); + +// Or use HeaderParam for dynamic headers +[Get("/data")] +Task GetData([HeaderParam("X-Custom-Header")] string customValue); +``` + +### Form Data + +```csharp +[Post("/login")] +[ContentType("application/x-www-form-urlencoded")] +Task Login([FormParam] string username, [FormParam] string password); +``` + +### Multipart Form Data + +```csharp +[Post("/upload")] +[ContentType("multipart/form-data")] +Task UploadFile([PartParam] string description, [PartParam] Stream file); +``` + +### Synchronous Methods + +Both synchronous and asynchronous methods are supported: + +```csharp +// Async (recommended) +Task GetMovieAsync(string id); + +// Sync +Movie GetMovie(string id); +``` + +## Troubleshooting + +### Common Issues + +1. **Null values in parameters**: Null values mean no parameter is sent. Use empty string to send a parameter without a value. + +2. **JSON serialization errors**: Ensure you've installed and configured either `Activout.RestClient.Json` or `Activout.RestClient.Newtonsoft.Json`. + +3. **Path parameter not replaced**: Ensure the parameter name in `{paramName}` matches the method parameter name or use `[PathParam("paramName")]`. + +4. **MockHttp not matching requests**: Check the exact URL, headers, and HTTP method. Use `_mockHttp.GetMatchCount()` to debug. + +## Additional Resources + +- [Activout.RestClient GitHub Repository](https://github.com/twogood/Activout.RestClient) +- [RichardSzalay.MockHttp Documentation](https://github.com/richardszalay/mockhttp) +- [System.Text.Json Documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-overview) +- [Newtonsoft.Json Documentation](https://www.newtonsoft.com/json/help/html/Introduction.htm) + +## Support + +Need help implementing Activout.RestClient? Contact [david@activout.se](mailto:david@activout.se) to order a support package. + +## License + +Activout.RestClient is licensed under the [MIT License](https://github.com/twogood/Activout.RestClient/blob/main/LICENSE). From c5ac37e34b4232a3a0c9a376fbf192fdd4cc4f58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:22:56 +0000 Subject: [PATCH 3/4] Fix instructions.md based on code review feedback Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- instructions.md | 85 ++++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/instructions.md b/instructions.md index 8b9f7af..98f3d60 100644 --- a/instructions.md +++ b/instructions.md @@ -26,37 +26,37 @@ Create an interface that describes your REST API endpoints. Use attributes to sp ```csharp using Activout.RestClient; -[Path("api/movies")] +[Path("movies")] [ErrorResponse(typeof(ErrorResponse))] [Accept("application/json")] [ContentType("application/json")] public interface IMovieReviewService { - // GET /api/movies + // GET /movies [Get] Task> GetAllMovies(); - // GET /api/movies/{movieId}/reviews + // GET /movies/{movieId}/reviews [Get("/{movieId}/reviews")] Task> GetAllReviews(string movieId); - // GET /api/movies/{movieId}/reviews/{reviewId} + // GET /movies/{movieId}/reviews/{reviewId} [Get("/{movieId}/reviews/{reviewId}")] Task GetReview(string movieId, string reviewId); - // POST /api/movies/{movieId}/reviews + // POST /movies/{movieId}/reviews [Post("/{movieId}/reviews")] Task SubmitReview(string movieId, Review review); - // PUT /api/movies/{movieId}/reviews/{reviewId} + // PUT /movies/{movieId}/reviews/{reviewId} [Put("/{movieId}/reviews/{reviewId}")] Task UpdateReview(string movieId, string reviewId, Review review); - // DELETE /api/movies/{movieId}/reviews/{reviewId} + // DELETE /movies/{movieId}/reviews/{reviewId} [Delete("/{movieId}/reviews/{reviewId}")] Task DeleteReview(string movieId, string reviewId); - // GET /api/movies?begin=...&end=... + // GET /movies?begin=...&end=... [Get] Task> QueryMoviesByDate( [QueryParam] DateTime begin, @@ -91,7 +91,6 @@ Define your request and response models as regular C# classes or records: public class Movie { public string? Title { get; init; } - public int? Year { get; init; } } public class Review @@ -110,13 +109,13 @@ public class Review public class ErrorResponse { - public List? Errors { get; init; } -} + public List Errors { get; init; } = []; -public class ErrorDetail -{ - public int Code { get; init; } - public string? Message { get; init; } + public class Error + { + public int Code { get; init; } + public string? Message { get; init; } + } } ``` @@ -161,31 +160,36 @@ var movies = await movieService.GetAllMovies(); ### Using with Dependency Injection -Add the required services to your `IServiceCollection`: +You can register the required RestClient services and your API clients in your DI container. Here's an example using ASP.NET Core's `IServiceCollection`: ```csharp using Activout.RestClient; using Activout.RestClient.Helpers.Implementation; using Activout.RestClient.ParamConverter; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; -public static IServiceCollection AddRestClient(this IServiceCollection services) +public static class ServiceCollectionExtensions { - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - return services; + public static IServiceCollection AddRestClient(this IServiceCollection services) + { + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + return services; + } } -// In your Startup.cs or Program.cs +// In your Program.cs or Startup.cs services.AddRestClient(); services.AddHttpClient(); // Register your specific API client services.AddSingleton(provider => { - var httpClient = provider.GetRequiredService().CreateClient(); + var httpClientFactory = provider.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient(); var restClientFactory = provider.GetRequiredService(); return restClientFactory.CreateBuilder() @@ -196,6 +200,8 @@ services.AddSingleton(provider => }); ``` +**Note:** The core Activout.RestClient library doesn't include built-in DI extensions. The above code demonstrates how to create your own extension methods to register the necessary services. + ## Step 4: Unit Testing with MockHttp Use `RichardSzalay.MockHttp` to mock HTTP responses in your unit tests without making real HTTP calls. @@ -239,9 +245,9 @@ public async Task GetAllMovies_ReturnsMovieList() { // Arrange _mockHttp - .When($"{BaseUri}/api/movies") + .When($"{BaseUri}/movies") .WithHeaders("Accept", "application/json") - .Respond("application/json", "[{\"Title\":\"Inception\",\"Year\":2010}]"); + .Respond("application/json", "[{\"Title\":\"Inception\"}]"); var service = CreateMovieReviewService(); @@ -252,7 +258,6 @@ public async Task GetAllMovies_ReturnsMovieList() var movieList = movies.ToList(); Assert.Single(movieList); Assert.Equal("Inception", movieList[0].Title); - Assert.Equal(2010, movieList[0].Year); } [Fact] @@ -260,7 +265,7 @@ public async Task GetAllMovies_ReturnsEmptyList_WhenNoMovies() { // Arrange _mockHttp - .When($"{BaseUri}/api/movies") + .When($"{BaseUri}/movies") .Respond("application/json", "[]"); var service = CreateMovieReviewService(); @@ -282,7 +287,7 @@ public async Task SubmitReview_CreatesNewReview() // Arrange var movieId = "movie-123"; _mockHttp - .When(HttpMethod.Post, $"{BaseUri}/api/movies/{movieId}/reviews") + .When(HttpMethod.Post, $"{BaseUri}/movies/{movieId}/reviews") .WithHeaders("Content-Type", "application/json; charset=utf-8") .Respond(request => { @@ -315,7 +320,7 @@ public async Task UpdateReview_ModifiesExistingReview() var movieId = "movie-123"; var reviewId = "review-456"; _mockHttp - .When(HttpMethod.Put, $"{BaseUri}/api/movies/{movieId}/reviews/{reviewId}") + .When(HttpMethod.Put, $"{BaseUri}/movies/{movieId}/reviews/{reviewId}") .Respond(request => request.Content!); // Echo back the request var service = CreateMovieReviewService(); @@ -341,8 +346,8 @@ public async Task QueryMoviesByDate_UsesQueryParameters() var end = new DateTime(2020, 12, 31, 0, 0, 0, DateTimeKind.Utc); _mockHttp - .When($"{BaseUri}/api/movies?begin=2020-01-01T00%3A00%3A00.0000000Z&end=2020-12-31T00%3A00%3A00.0000000Z") - .Respond("application/json", "[{\"Title\":\"Tenet\",\"Year\":2020}]"); + .When($"{BaseUri}/movies?begin=2020-01-01T00%3A00%3A00.0000000Z&end=2020-12-31T00%3A00%3A00.0000000Z") + .Respond("application/json", "[{\"Title\":\"Tenet\"}]"); var service = CreateMovieReviewService(); @@ -367,7 +372,7 @@ public async Task GetReview_ThrowsRestClientException_WhenNotFound() var reviewId = "invalid-review"; _mockHttp - .Expect(HttpMethod.Get, $"{BaseUri}/api/movies/{movieId}/reviews/{reviewId}") + .Expect(HttpMethod.Get, $"{BaseUri}/movies/{movieId}/reviews/{reviewId}") .Respond(HttpStatusCode.NotFound, _ => new StringContent( JsonConvert.SerializeObject(new { @@ -389,7 +394,7 @@ public async Task GetReview_ThrowsRestClientException_WhenNotFound() var error = exception.GetErrorResponse(); Assert.NotNull(error); - Assert.Equal(404, error.Errors![0].Code); + Assert.Equal(404, error.Errors[0].Code); Assert.Equal("Review not found", error.Errors[0].Message); // Verify the expected call was made @@ -403,13 +408,13 @@ MockHttp provides two ways to set up expectations: **`When()`** - Matches any number of requests: ```csharp -_mockHttp.When($"{BaseUri}/api/movies") +_mockHttp.When($"{BaseUri}/movies") .Respond("application/json", "[]"); ``` **`Expect()`** - Expects exactly one request: ```csharp -_mockHttp.Expect($"{BaseUri}/api/movies") +_mockHttp.Expect($"{BaseUri}/movies") .Respond("application/json", "[]"); // Verify all expectations were met @@ -493,7 +498,7 @@ public class MovieReviewServiceTests public async Task GetAllMovies_Success() { _mockHttp - .When($"{BaseUri}/api/movies") + .When($"{BaseUri}/movies") .Respond("application/json", "[{\"Title\":\"Inception\"}]"); var service = CreateService(); @@ -506,7 +511,7 @@ public class MovieReviewServiceTests public async Task SubmitReview_Success() { _mockHttp - .When(HttpMethod.Post, $"{BaseUri}/api/movies/123/reviews") + .When(HttpMethod.Post, $"{BaseUri}/movies/123/reviews") .Respond(request => { var content = request.Content!.ReadAsStringAsync().Result; @@ -526,7 +531,7 @@ public class MovieReviewServiceTests public async Task GetReview_NotFound_ThrowsException() { _mockHttp - .When(HttpMethod.Get, $"{BaseUri}/api/movies/123/reviews/999") + .When(HttpMethod.Get, $"{BaseUri}/movies/123/reviews/999") .Respond(HttpStatusCode.NotFound, _ => new StringContent( JsonConvert.SerializeObject(new { @@ -542,7 +547,7 @@ public class MovieReviewServiceTests Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); var error = exception.GetErrorResponse(); - Assert.Equal(404, error!.Errors![0].Code); + Assert.Equal(404, error!.Errors[0].Code); } } ``` From 4910881eb3d738bc2dd5464320493a17eb8d5ba7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:25:45 +0000 Subject: [PATCH 4/4] Update return types to IEnumerable for consistency with actual codebase Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- instructions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instructions.md b/instructions.md index 98f3d60..b18eec1 100644 --- a/instructions.md +++ b/instructions.md @@ -34,7 +34,7 @@ public interface IMovieReviewService { // GET /movies [Get] - Task> GetAllMovies(); + Task> GetAllMovies(); // GET /movies/{movieId}/reviews [Get("/{movieId}/reviews")] @@ -58,7 +58,7 @@ public interface IMovieReviewService // GET /movies?begin=...&end=... [Get] - Task> QueryMoviesByDate( + Task> QueryMoviesByDate( [QueryParam] DateTime begin, [QueryParam] DateTime end); }