A:初版
This commit is contained in:
1 parent
237cbd8788
commit
ab3721912d
102 files changed
+7929
-20
No files matched your search
@@ -0,0 +1 @@
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="MSTest" Version="3.6.4"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Sharp\NetClient\NetClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace NetClient.Test;
|
||||
|
||||
[TestClass]
|
||||
public sealed class Test1
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestMethod1()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NetClient.Abstractions;
|
||||
|
||||
namespace NetClient.Test.TestDoubles;
|
||||
|
||||
public sealed class StaticTokenProvider : IAccessTokenProvider
|
||||
{
|
||||
private readonly string? _token;
|
||||
|
||||
public StaticTokenProvider(string? token)
|
||||
{
|
||||
_token = token;
|
||||
}
|
||||
|
||||
public Task<string?> GetTokenAsync(CancellationToken cancellationToken = default) => Task.FromResult(_token);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NetClient.Test.TestDoubles;
|
||||
|
||||
public sealed class TestHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>> _handler;
|
||||
private int _calls;
|
||||
|
||||
public int Calls => _calls;
|
||||
|
||||
public TestHttpMessageHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> handler)
|
||||
{
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _calls);
|
||||
return await _handler(request);
|
||||
}
|
||||
|
||||
public static HttpResponseMessage Json(HttpStatusCode code, string json)
|
||||
=> new(code) { Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json") };
|
||||
|
||||
public static HttpResponseMessage Text(HttpStatusCode code, string text)
|
||||
=> new(code) { Content = new StringContent(text) };
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NetClient.Abstractions;
|
||||
|
||||
namespace NetClient.Test.TestDoubles;
|
||||
|
||||
public sealed class TestJsonSerializer : IHttpContentSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
public TestJsonSerializer(JsonSerializerOptions? options = null)
|
||||
{
|
||||
_options = options ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
}
|
||||
|
||||
public string ContentType => "application/json";
|
||||
|
||||
public Task SerializeAsync<T>(Stream stream, T? value, CancellationToken cancellationToken = default)
|
||||
=> JsonSerializer.SerializeAsync(stream, value, _options, cancellationToken);
|
||||
|
||||
public Task<T?> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
|
||||
=> JsonSerializer.DeserializeAsync<T>(stream, _options, cancellationToken).AsTask();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NetClient.Abstractions;
|
||||
using NetClient.DependencyInjection;
|
||||
|
||||
namespace NetClient.Test.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class NamedClientFactoryTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Factory_Creates_Named_Clients_With_Different_Configs()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddNamedNetClient("catalog", opt =>
|
||||
{
|
||||
opt.BaseAddress = new Uri("https://catalog.test");
|
||||
}).ConfigurePrimaryHttpMessageHandler(() => new TestDoubles.TestHttpMessageHandler(req =>
|
||||
{
|
||||
StringAssert.StartsWith(req.RequestUri!.AbsoluteUri, "https://catalog.test");
|
||||
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "catalog"));
|
||||
}));
|
||||
|
||||
services.AddNamedNetClient("order", opt =>
|
||||
{
|
||||
opt.BaseAddress = new Uri("https://order.test");
|
||||
}).ConfigurePrimaryHttpMessageHandler(() => new TestDoubles.TestHttpMessageHandler(req =>
|
||||
{
|
||||
StringAssert.StartsWith(req.RequestUri!.AbsoluteUri, "https://order.test");
|
||||
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "order"));
|
||||
}));
|
||||
|
||||
var sp = services.BuildServiceProvider();
|
||||
var factory = sp.GetRequiredService<INetClientFactory>();
|
||||
|
||||
var c = factory.Create("catalog");
|
||||
var o = factory.Create("order");
|
||||
|
||||
var cRes = await c.GetStringAsync("/ping");
|
||||
var oRes = await o.GetStringAsync("/ping");
|
||||
|
||||
Assert.AreEqual("catalog", cRes);
|
||||
Assert.AreEqual("order", oRes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NetClient.Abstractions;
|
||||
using NetClient.Exceptions;
|
||||
using NetClient.Implementations;
|
||||
using NetClient.Test.TestDoubles;
|
||||
|
||||
namespace NetClient.Test.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class NetClientBasicTests
|
||||
{
|
||||
private static DefaultNetClient CreateClient(HttpMessageHandler handler)
|
||||
{
|
||||
var http = new HttpClient(handler) { BaseAddress = new Uri("https://example.com") };
|
||||
IHttpContentSerializer serializer = new TestJsonSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
var logger = NullLogger<DefaultNetClient>.Instance;
|
||||
return new DefaultNetClient(http, serializer, logger);
|
||||
}
|
||||
|
||||
private sealed record EchoDto(string Method, string Path, string? Query, string? Name);
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAsync_Deserializes_And_Builds_Query()
|
||||
{
|
||||
var handler = new TestHttpMessageHandler(req =>
|
||||
{
|
||||
Assert.AreEqual(HttpMethod.Get, req.Method);
|
||||
Assert.AreEqual("https://example.com/api/echo", $"{req.RequestUri!.Scheme}://{req.RequestUri.Host}{req.RequestUri.AbsolutePath}");
|
||||
var q = req.RequestUri!.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||
CollectionAssert.IsSubsetOf(new[] { "id=1", "name=foo" }, q);
|
||||
var json = $"{{\"method\":\"{req.Method}\",\"path\":\"{req.RequestUri!.AbsolutePath}\",\"query\":\"{req.RequestUri!.Query.TrimStart('?')}\",\"name\":\"foo\"}}";
|
||||
return Task.FromResult(TestHttpMessageHandler.Json(HttpStatusCode.OK, json));
|
||||
});
|
||||
|
||||
var client = CreateClient(handler);
|
||||
var dto = await client.GetAsync<EchoDto>("/api/echo", new { id = 1, name = "foo" });
|
||||
|
||||
Assert.IsNotNull(dto);
|
||||
Assert.AreEqual("GET", dto!.Method);
|
||||
Assert.AreEqual("/api/echo", dto.Path);
|
||||
Assert.IsTrue(dto.Query!.Contains("id=1"));
|
||||
Assert.IsTrue(dto.Query!.Contains("name=foo"));
|
||||
Assert.AreEqual("foo", dto.Name);
|
||||
}
|
||||
|
||||
private sealed record OkDto(bool ok);
|
||||
|
||||
[TestMethod]
|
||||
public async Task PostAsync_Sends_Json_With_ContentType()
|
||||
{
|
||||
var handler = new TestHttpMessageHandler(async req =>
|
||||
{
|
||||
Assert.AreEqual(HttpMethod.Post, req.Method);
|
||||
Assert.AreEqual("application/json", req.Content!.Headers.ContentType!.MediaType);
|
||||
Assert.AreEqual("utf-8", req.Content!.Headers.ContentType!.CharSet?.ToLowerInvariant());
|
||||
var body = await req.Content!.ReadAsStringAsync();
|
||||
Assert.IsTrue(body.Contains("\"name\":\"bar\""));
|
||||
return TestHttpMessageHandler.Json(HttpStatusCode.OK, "{\"ok\":true}");
|
||||
});
|
||||
|
||||
var client = CreateClient(handler);
|
||||
var res = await client.PostAsync<object, OkDto>("/api/items", new { name = "bar" });
|
||||
|
||||
Assert.IsNotNull(res);
|
||||
Assert.AreEqual(true, res!.ok);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Error_Throws_ApiException_With_Content()
|
||||
{
|
||||
var handler = new TestHttpMessageHandler(req => Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.InternalServerError, "oops")));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
try
|
||||
{
|
||||
await client.GetAsync<JsonElement>("/api/error");
|
||||
Assert.Fail("Expected ApiException");
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
Assert.AreEqual(HttpStatusCode.InternalServerError, ex.StatusCode);
|
||||
StringAssert.Contains(ex.ResponseContent ?? string.Empty, "oops");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NetClient.Abstractions;
|
||||
using NetClient.DependencyInjection;
|
||||
using NetClient.Test.TestDoubles;
|
||||
|
||||
namespace NetClient.Test.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class NetClientDiTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task AddNetClient_TypedClient_Applies_Options_And_Handlers()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IAccessTokenProvider>(new StaticTokenProvider("token-123"));
|
||||
|
||||
TestHttpMessageHandler? capturedHandler = null;
|
||||
var builder = services.AddNetClient(opt =>
|
||||
{
|
||||
opt.BaseAddress = new Uri("https://api.test");
|
||||
opt.UseBearerToken = true;
|
||||
opt.IncludeCorrelationId = true;
|
||||
});
|
||||
|
||||
builder.ConfigurePrimaryHttpMessageHandler(() =>
|
||||
{
|
||||
capturedHandler = new TestHttpMessageHandler(req =>
|
||||
{
|
||||
// BaseAddress + Path combined
|
||||
StringAssert.StartsWith(req.RequestUri!.AbsoluteUri, "https://api.test");
|
||||
// Correlation header present
|
||||
Assert.IsTrue(req.Headers.Contains("X-Correlation-Id"));
|
||||
// Authorization applied
|
||||
Assert.AreEqual("Bearer", req.Headers.Authorization!.Scheme);
|
||||
Assert.AreEqual("token-123", req.Headers.Authorization!.Parameter);
|
||||
return Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.OK, "ok"));
|
||||
});
|
||||
return capturedHandler;
|
||||
});
|
||||
|
||||
var sp = services.BuildServiceProvider();
|
||||
var client = sp.GetRequiredService<IHttpClient>();
|
||||
var result = await client.GetStringAsync("/ping");
|
||||
|
||||
Assert.AreEqual("ok", result);
|
||||
Assert.IsNotNull(capturedHandler);
|
||||
Assert.AreEqual(1, capturedHandler!.Calls);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Common.Results;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NetClient.Abstractions;
|
||||
using NetClient.Implementations;
|
||||
using NetClient.Test.TestDoubles;
|
||||
|
||||
namespace NetClient.Test.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class NetClientResultsTests
|
||||
{
|
||||
private static DefaultNetClient CreateClient(HttpMessageHandler handler)
|
||||
{
|
||||
var http = new HttpClient(handler) { BaseAddress = new Uri("https://example.com") };
|
||||
IHttpContentSerializer serializer = new TestJsonSerializer();
|
||||
var logger = NullLogger<DefaultNetClient>.Instance;
|
||||
return new DefaultNetClient(http, serializer, logger);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetResultAsync_Success_Returns_Result_With_Value()
|
||||
{
|
||||
var json = "{\"id\":1}";
|
||||
var handler = new TestHttpMessageHandler(_ => Task.FromResult(TestHttpMessageHandler.Json(HttpStatusCode.OK, json)));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
var res = await client.GetResultAsync<Obj>("/obj");
|
||||
Assert.IsTrue(res.IsSuccess);
|
||||
Assert.AreEqual(1, res.Value!.id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetResultAsync_Failure_Maps_StatusCode_And_Message()
|
||||
{
|
||||
var handler = new TestHttpMessageHandler(_ => Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.BadRequest, "bad req")));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
var res = await client.GetResultAsync<Obj>("/obj");
|
||||
Assert.IsTrue(res.IsFailure);
|
||||
Assert.AreEqual((long)HttpStatusCode.BadRequest, res.ErrorCode);
|
||||
StringAssert.Contains(res.ErrorMessage ?? string.Empty, "bad req");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DeleteResultAsync_Success_Returns_Success_Result()
|
||||
{
|
||||
var handler = new TestHttpMessageHandler(_ => Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.NoContent, "")));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
var res = await client.DeleteResultAsync("/obj/1");
|
||||
Assert.IsTrue(res.IsSuccess);
|
||||
}
|
||||
|
||||
private sealed record Obj(int id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NetClient.Abstractions;
|
||||
using NetClient.DependencyInjection;
|
||||
|
||||
namespace NetClient.Test.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class NetClientRetryTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Retry_On_429_Then_Succeed()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var attempts = 0;
|
||||
|
||||
var builder = services.AddNetClient(opt =>
|
||||
{
|
||||
opt.BaseAddress = new Uri("https://retry.test");
|
||||
opt.Retry.Enabled = true;
|
||||
opt.Retry.MaxRetryAttempts = 2;
|
||||
opt.Retry.FirstDelay = TimeSpan.FromMilliseconds(5);
|
||||
opt.Retry.Handle429 = true;
|
||||
});
|
||||
|
||||
builder.ConfigurePrimaryHttpMessageHandler(() => new TestDoubles.TestHttpMessageHandler(req =>
|
||||
{
|
||||
attempts++;
|
||||
if (attempts == 1)
|
||||
{
|
||||
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.TooManyRequests, "busy"));
|
||||
}
|
||||
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "ok"));
|
||||
}));
|
||||
|
||||
var sp = services.BuildServiceProvider();
|
||||
var client = sp.GetRequiredService<IHttpClient>();
|
||||
|
||||
var result = await client.GetStringAsync("/ping");
|
||||
|
||||
Assert.AreEqual("ok", result);
|
||||
Assert.AreEqual(2, attempts);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user