diff --git a/Activout.RestClient.Test.Json/Activout.RestClient.Test.Json.csproj b/Activout.RestClient.Test.Json/Activout.RestClient.Test.Json.csproj new file mode 100644 index 0000000..7b053ca --- /dev/null +++ b/Activout.RestClient.Test.Json/Activout.RestClient.Test.Json.csproj @@ -0,0 +1,31 @@ + + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/LoggerFactoryHelpers.cs b/Activout.RestClient.Test.Json/LoggerFactoryHelpers.cs new file mode 100644 index 0000000..d4ff212 --- /dev/null +++ b/Activout.RestClient.Test.Json/LoggerFactoryHelpers.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Activout.RestClient.Test.Json; + +public static class LoggerFactoryHelpers +{ + public static ILoggerFactory CreateLoggerFactory(ITestOutputHelper outputHelper) + { + return LoggerFactory.Create(builder => + { + builder + .AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning) + .AddFilter("Activout.RestClient", LogLevel.Debug) + .AddXUnit(outputHelper); + }); + } +} \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/MovieReviews/ErrorResponse.cs b/Activout.RestClient.Test.Json/MovieReviews/ErrorResponse.cs new file mode 100644 index 0000000..7a22081 --- /dev/null +++ b/Activout.RestClient.Test.Json/MovieReviews/ErrorResponse.cs @@ -0,0 +1,12 @@ +namespace Activout.RestClient.Test.Json.MovieReviews; + +public class ErrorResponse +{ + public List Errors { get; set; } = []; + + public class Error + { + public string Message { get; set; } = string.Empty; + public int Code { get; set; } + } +} \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/MovieReviews/IMovieReviewService.cs b/Activout.RestClient.Test.Json/MovieReviews/IMovieReviewService.cs new file mode 100644 index 0000000..5ed2a89 --- /dev/null +++ b/Activout.RestClient.Test.Json/MovieReviews/IMovieReviewService.cs @@ -0,0 +1,36 @@ +namespace Activout.RestClient.Test.Json.MovieReviews; + +[Path("movies")] +[ErrorResponse(typeof(ErrorResponse))] +public interface IMovieReviewService +{ + [Get] + Task> GetAllMovies(); + + [Get] + Task> GetAllMoviesCancellable(CancellationToken cancellationToken); + + [Get] + [Path("/{movieId}/reviews")] + Task> GetAllReviews(string movieId); + + [Get("/{movieId}/reviews/{reviewId}")] + Review GetReview(string movieId, string reviewId); + + [Post] + [Path("/{movieId}/reviews")] + Task SubmitReview([PathParam("movieId")] string movieId, Review review); + + [Put] + [Path("/{movieId}/reviews/{reviewId}")] + Review UpdateReview(string movieId, [PathParam] string reviewId, Review review); + + [Patch] + [Path("/{movieId}/reviews/{reviewId}")] + Review PartialUpdateReview(string movieId, [PathParam] string reviewId, Review review); + + [Get] + Task> QueryMoviesByDate( + [QueryParam] DateTime begin, + [QueryParam] DateTime end); +} \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/MovieReviews/Movie.cs b/Activout.RestClient.Test.Json/MovieReviews/Movie.cs new file mode 100644 index 0000000..c14b95f --- /dev/null +++ b/Activout.RestClient.Test.Json/MovieReviews/Movie.cs @@ -0,0 +1,9 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Activout.RestClient.Test.Json.MovieReviews; + +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +public class Movie +{ + public string Title { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/MovieReviews/Review.cs b/Activout.RestClient.Test.Json/MovieReviews/Review.cs new file mode 100644 index 0000000..85b9568 --- /dev/null +++ b/Activout.RestClient.Test.Json/MovieReviews/Review.cs @@ -0,0 +1,15 @@ +namespace Activout.RestClient.Test.Json.MovieReviews; + +public class Review +{ + public Review(int stars, string text) + { + Stars = stars; + Text = text; + } + + public string? MovieId { get; set; } + public string? ReviewId { get; set; } + public int Stars { get; set; } + public string Text { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/RestClientTests.cs b/Activout.RestClient.Test.Json/RestClientTests.cs new file mode 100644 index 0000000..efab20e --- /dev/null +++ b/Activout.RestClient.Test.Json/RestClientTests.cs @@ -0,0 +1,310 @@ +using Activout.RestClient.Helpers.Implementation; +using Activout.RestClient.Json; +using Activout.RestClient.Newtonsoft.Json; +using Activout.RestClient.Test.Json.MovieReviews; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RichardSzalay.MockHttp; +using System.Net; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +namespace Activout.RestClient.Test.Json; + +public enum JsonImplementation +{ + SystemTextJson, + NewtonsoftJson +} + +public class RestClientTests(ITestOutputHelper 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 = Services.CreateRestClientFactory(); + private readonly MockHttpMessageHandler _mockHttp = new(); + private readonly ILoggerFactory _loggerFactory = LoggerFactoryHelpers.CreateLoggerFactory(outputHelper); + + private IRestClientBuilder CreateRestClientBuilder(JsonImplementation jsonImplementation) + { + var builder = _restClientFactory.CreateBuilder(); + + return jsonImplementation switch + { + JsonImplementation.SystemTextJson => builder.WithSystemTextJson(), + JsonImplementation.NewtonsoftJson => builder.WithNewtonsoftJson(), + _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation)) + }; + } + + private IMovieReviewService CreateMovieReviewService(JsonImplementation jsonImplementation) + { + return CreateRestClientBuilder(jsonImplementation) + .Accept("application/json") + .ContentType("application/json") + .With(_loggerFactory.CreateLogger()) + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri) + .Build(); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestErrorAsyncWithOldTaskConverter(JsonImplementation jsonImplementation) + { + // arrange + ExpectGetAllReviewsAndReturnError(); + + var reviewSvc = CreateRestClientBuilder(jsonImplementation) + .Accept("application/json") + .ContentType("application/json") + .With(new TaskConverterFactory()) + .With(_loggerFactory.CreateLogger()) + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri) + .Build(); + + // act + var aggregateException = + await Assert.ThrowsAsync(() => reviewSvc.GetAllReviews(MovieId)); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + + Assert.IsType(aggregateException.InnerException); + var exception = (RestClientException)aggregateException.InnerException!; + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + var error = exception.GetErrorResponse(); + Assert.Equal(34, error.Errors[0].Code); + Assert.Equal("Sorry, that page does not exist", error.Errors[0].Message); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestErrorAsync(JsonImplementation jsonImplementation) + { + // arrange + ExpectGetAllReviewsAndReturnError(); + + var reviewSvc = CreateMovieReviewService(jsonImplementation); + + // act + var exception = + await Assert.ThrowsAsync(() => reviewSvc.GetAllReviews(MovieId)); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + var error = exception.GetErrorResponse(); + Assert.Equal(34, error.Errors[0].Code); + Assert.Equal("Sorry, that page does not exist", error.Errors[0].Message); + } + + private void ExpectGetAllReviewsAndReturnError(string movieId = MovieId) + { + _mockHttp + .Expect(HttpMethod.Get, $"{BaseUri}/movies/{movieId}/reviews") + .Respond(HttpStatusCode.NotFound, request => new StringContent(JsonConvert.SerializeObject(new + { + Errors = new object[] + { + new {Message = "Sorry, that page does not exist", Code = 34} + } + }), + Encoding.UTF8, + "application/json")); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public void TestErrorSync(JsonImplementation jsonImplementation) + { + // arrange + _mockHttp + .When(HttpMethod.Get, $"{BaseUri}/movies/{MovieId}/reviews/{ReviewId}") + .Respond(HttpStatusCode.NotFound, request => new StringContent(JsonConvert.SerializeObject(new + { + Errors = new object[] + { + new {Message = "Sorry, that page does not exist", Code = 34} + } + }), + Encoding.UTF8, + "application/json")); + + var reviewSvc = CreateMovieReviewService(jsonImplementation); + + // act + var aggregateException = Assert.Throws(() => reviewSvc.GetReview(MovieId, ReviewId)); + + // assert + var exception = (RestClientException)aggregateException.GetBaseException(); + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + + dynamic dynamicError = exception.ErrorResponse!; + string message = dynamicError.Errors[0].Message; + int code = dynamicError.Errors[0].Code; + Assert.Equal(34, code); + Assert.Equal("Sorry, that page does not exist", message); + + var error = exception.GetErrorResponse(); + Assert.Equal(34, error.Errors[0].Code); + Assert.Equal("Sorry, that page does not exist", error.Errors[0].Message); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestGetEmptyIEnumerableAsync(JsonImplementation jsonImplementation) + { + // arrange + _mockHttp + .When($"{BaseUri}/movies") + .WithHeaders("Accept", "application/json") + .Respond("application/json", "[]"); + + var reviewSvc = CreateMovieReviewService(jsonImplementation); + + // act + var movies = await reviewSvc.GetAllMovies(); + + // assert + Assert.Empty(movies); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestQueryParamAsync(JsonImplementation jsonImplementation) + { + // arrange + _mockHttp + .When($"{BaseUri}/movies?begin=2017-01-01T00%3A00%3A00.0000000Z&end=2018-01-01T00%3A00%3A00.0000000Z") + .Respond("application/json", "[{\"Title\":\"Blade Runner 2049\"}]"); + + var reviewSvc = CreateMovieReviewService(jsonImplementation); + + // act + var movies = await reviewSvc.QueryMoviesByDate( + new DateTime(2017, 1, 1, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2018, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + + // assert + var list = movies.ToList(); + Assert.Single(list); + var movie = list.First(); + Assert.Equal("Blade Runner 2049", movie.Title); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestPostJsonAsync(JsonImplementation jsonImplementation) + { + // arrange + var movieId = "FOOBAR"; + + // The replacement logic needs to handle different JSON formats + _mockHttp + .When(HttpMethod.Post, $"{BaseUri}/movies/{movieId}/reviews") + .WithHeaders("Content-Type", "application/json; charset=utf-8") + .Respond(request => + { + var content = request.Content!.ReadAsStringAsync().Result; + + // Handle different serialization formats + content = jsonImplementation switch + { + JsonImplementation.SystemTextJson => content.Replace("\"reviewId\":null", "\"reviewId\":\"*REVIEW_ID*\"") + .Replace("}", ",\"reviewId\":\"*REVIEW_ID*\"}"), // Add reviewId if not present + JsonImplementation.NewtonsoftJson => content.Replace("\"ReviewId\":null", "\"ReviewId\":\"*REVIEW_ID*\""), + _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation)) + }; + + return new StringContent(content, Encoding.UTF8, "application/json"); + }); + + var reviewSvc = CreateMovieReviewService(jsonImplementation); + + // act + var text = "This was a delightful comedy, but not terribly realistic."; + var stars = 3; + var review = new Review(stars, text); + var result = await reviewSvc.SubmitReview(movieId, review); + + // assert + Assert.Equal("*REVIEW_ID*", result.ReviewId); + Assert.Equal(stars, result.Stars); + Assert.Equal(text, result.Text); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public void TestPutSync(JsonImplementation jsonImplementation) + { + // arrange + var movieId = "*MOVIE_ID*"; + var reviewId = "*REVIEW_ID*"; + _mockHttp + .When(HttpMethod.Put, $"{BaseUri}/movies/{movieId}/reviews/{reviewId}") + .Respond(request => request.Content!); + + var reviewSvc = CreateMovieReviewService(jsonImplementation); + + // act + var text = "This was actally really good!"; + var stars = 5; + var review = new Review(stars, text) + { + MovieId = movieId, + ReviewId = reviewId + }; + var result = reviewSvc.UpdateReview(movieId, reviewId, review); + + // assert + Assert.Equal(movieId, result.MovieId); + Assert.Equal(reviewId, result.ReviewId); + Assert.Equal(stars, result.Stars); + Assert.Equal(text, result.Text); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public void TestPatchSync(JsonImplementation jsonImplementation) + { + // arrange + var movieId = "*MOVIE_ID*"; + var reviewId = "*REVIEW_ID*"; + _mockHttp + .When(HttpMethod.Patch, $"{BaseUri}/movies/{movieId}/reviews/{reviewId}") + .Respond(request => request.Content!); + + var reviewSvc = CreateMovieReviewService(jsonImplementation); + + // act + var text = "This was actally really good!"; + var stars = 5; + var review = new Review(stars, text) + { + MovieId = movieId, + ReviewId = reviewId + }; + var result = reviewSvc.PartialUpdateReview(movieId, reviewId, review); + + // assert + Assert.Equal(movieId, result.MovieId); + Assert.Equal(reviewId, result.ReviewId); + Assert.Equal(stars, result.Stars); + Assert.Equal(text, result.Text); + } +} \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/SerializationOrderTest.cs b/Activout.RestClient.Test.Json/SerializationOrderTest.cs new file mode 100644 index 0000000..402f7f5 --- /dev/null +++ b/Activout.RestClient.Test.Json/SerializationOrderTest.cs @@ -0,0 +1,114 @@ +using Activout.RestClient.Json; +using Activout.RestClient.Newtonsoft.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using RichardSzalay.MockHttp; +using System.Net; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace Activout.RestClient.Test.Json; + +public class SerializationOrderTest +{ + private const string BaseUri = "https://example.com/"; + private const int OrderFirst = -1000; + private const int OrderLast = 1000; + + private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory(); + private readonly MockHttpMessageHandler _mockHttp = new(); + + [Theory] + [InlineData(OrderFirst, "snake")] + [InlineData(OrderLast, "camel")] + public async Task TestSerializationOrderNewtonsoft(int order, string expectedValue) + { + // Arrange + var client = CreateNewtonsoftClient(order); + + _mockHttp + .Expect(HttpMethod.Get, BaseUri) + .Respond(new StringContent(JsonConvert.SerializeObject(new + { + my_value = "snake", + MyValue = "camel" + }), + Encoding.UTF8, + "application/json")); + + // Act + var model = await client.GetValue(); + + // Assert + Assert.Equal(expectedValue, model.MyValue); + } + + [Theory] + [InlineData("camel")] // System.Text.Json uses camelCase by default and matches MyValue + public async Task TestSerializationOrderSystemTextJson(string expectedValue) + { + // Arrange + var client = CreateSystemTextJsonClient(); + + _mockHttp + .Expect(HttpMethod.Get, BaseUri) + .Respond(new StringContent(System.Text.Json.JsonSerializer.Serialize(new + { + myValue = "camel", // System.Text.Json maps MyValue to myValue (camelCase) + my_value = "snake" + }), + Encoding.UTF8, + "application/json")); + + // Act + var model = await client.GetValue(); + + // Assert + Assert.Equal(expectedValue, model.MyValue); + } + + public class SerializationOrderModel + { + public string MyValue { get; set; } = string.Empty; + } + + public interface ISerializationOrderClient + { + Task GetValue(); + } + + private ISerializationOrderClient CreateNewtonsoftClient(int orderOfJsonDeserializer) + { + return CreateRestClientBuilder() + .WithNewtonsoftJson() + .With(new NewtonsoftJsonDeserializer(new JsonSerializerSettings() + { + ContractResolver = new DefaultContractResolver() + { + NamingStrategy = new SnakeCaseNamingStrategy() + } + }) + { Order = orderOfJsonDeserializer }) + .Build(); + } + + private ISerializationOrderClient CreateSystemTextJsonClient() + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + return CreateRestClientBuilder() + .WithSystemTextJson(options) + .Build(); + } + + private IRestClientBuilder CreateRestClientBuilder() + { + return _restClientFactory.CreateBuilder() + .With(_mockHttp.ToHttpClient()) + .BaseUri(new Uri(BaseUri)); + } +} \ No newline at end of file diff --git a/Activout.RestClient.Test.Json/SimpleValueObjectTest.cs b/Activout.RestClient.Test.Json/SimpleValueObjectTest.cs new file mode 100644 index 0000000..8f51eed --- /dev/null +++ b/Activout.RestClient.Test.Json/SimpleValueObjectTest.cs @@ -0,0 +1,160 @@ +using Activout.RestClient.Json; +using Activout.RestClient.Newtonsoft.Json; +using Newtonsoft.Json; +using RichardSzalay.MockHttp; +using System.Net; +using System.Text; +using Xunit; + +namespace Activout.RestClient.Test.Json; + +public record MySimpleValueObject(string Value); + +public class ApiData +{ + public MySimpleValueObject? FooBar { get; set; } + public int? NullableInteger { get; set; } +} + +public interface IValueObjectClient +{ + Task GetData(); + + [Post] + Task SetData(ApiData wrapper); +} + +public class SimpleValueObjectTest +{ + private const string BaseUri = "https://example.com/api/"; + + private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory(); + private readonly MockHttpMessageHandler _mockHttp = new(); + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestSimpleValueObjectSerialization(JsonImplementation jsonImplementation) + { + // Arrange + var expectedContent = jsonImplementation switch + { + JsonImplementation.SystemTextJson => System.Text.Json.JsonSerializer.Serialize(new + { + fooBar = "foobar", + nullableInteger = 42 + }), + JsonImplementation.NewtonsoftJson => JsonConvert.SerializeObject(new + { + FooBar = "foobar", + NullableInteger = 42 + }), + _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation)) + }; + + _mockHttp + .Expect(HttpMethod.Post, BaseUri) + .WithContent(expectedContent) + .Respond(HttpStatusCode.OK); + + var client = CreateClient(jsonImplementation); + + var wrapper = new ApiData + { + FooBar = new MySimpleValueObject("foobar"), + NullableInteger = 42 + }; + + // Act + await client.SetData(wrapper); + + // Assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestSimpleValueObjectDeserialization(JsonImplementation jsonImplementation) + { + // Arrange + var responseContent = jsonImplementation switch + { + JsonImplementation.SystemTextJson => System.Text.Json.JsonSerializer.Serialize(new + { + FooBar = "foobar", + NullableInteger = 42 + }), + JsonImplementation.NewtonsoftJson => JsonConvert.SerializeObject(new + { + FooBar = "foobar", + NullableInteger = 42 + }), + _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation)) + }; + + _mockHttp + .Expect(BaseUri) + .Respond(new StringContent(responseContent, Encoding.UTF8, "application/json")); + + var client = CreateClient(jsonImplementation); + + // Act + var result = await client.GetData(); + + // Assert + _mockHttp.VerifyNoOutstandingExpectation(); + Assert.Equal("foobar", result.FooBar?.Value); + Assert.Equal(42, result.NullableInteger); + } + + [Theory] + [InlineData(JsonImplementation.SystemTextJson)] + [InlineData(JsonImplementation.NewtonsoftJson)] + public async Task TestSimpleValueObjectDeserializationWithNulls(JsonImplementation jsonImplementation) + { + // Arrange + var responseContent = jsonImplementation switch + { + JsonImplementation.SystemTextJson => System.Text.Json.JsonSerializer.Serialize(new + { + FooBar = (string?)null, + NullableInteger = (int?)null + }), + JsonImplementation.NewtonsoftJson => JsonConvert.SerializeObject(new + { + FooBar = (string?)null, + NullableInteger = (int?)null + }), + _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation)) + }; + + _mockHttp + .Expect(BaseUri) + .Respond(new StringContent(responseContent, Encoding.UTF8, "application/json")); + + var client = CreateClient(jsonImplementation); + + // Act + var result = await client.GetData(); + + // Assert + _mockHttp.VerifyNoOutstandingExpectation(); + Assert.Null(result.FooBar); + Assert.Null(result.NullableInteger); + } + + private IValueObjectClient CreateClient(JsonImplementation jsonImplementation) + { + var builder = _restClientFactory.CreateBuilder() + .With(_mockHttp.ToHttpClient()) + .BaseUri(new Uri(BaseUri)); + + return jsonImplementation switch + { + JsonImplementation.SystemTextJson => builder.WithSystemTextJson().Build(), + JsonImplementation.NewtonsoftJson => builder.WithNewtonsoftJson().Build(), + _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation)) + }; + } +} \ No newline at end of file diff --git a/ActivoutRestClient.sln b/ActivoutRestClient.sln index 56767a2..d2b072c 100644 --- a/ActivoutRestClient.sln +++ b/ActivoutRestClient.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Activout.RestClient.Json", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Activout.RestClient.Json.Test", "Activout.RestClient.Json.Test\Activout.RestClient.Json.Test.csproj", "{690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Activout.RestClient.Test.Json", "Activout.RestClient.Test.Json\Activout.RestClient.Test.Json.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}.Debug|Any CPU.Build.0 = Debug|Any CPU {690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE