using System.Net; using System.Net.Http.Json; using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; using Asp.Versioning; using MAX.Framework.Commons; using MAX.Framework.Conventions; using MAX.Framework.Dtos; using MAX.Framework.Security; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Store.Shops.Controllers; using Store.Shops.Entities; using Store.Shops.Services; using Store.Users.Contracts; namespace Store.Shops.Tests; public sealed class ShopHttpTests : IAsyncLifetime { private readonly ShopTestContext context = new(); private WebApplication app = null!; private HttpClient client = null!; public async Task InitializeAsync() { var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = "Testing" }); builder.WebHost.UseTestServer(); builder.Logging.ClearProviders(); builder.Services.AddControllers(options => { options.Conventions.Add(new ApiRoutePrefixConvention()); options.Filters.Add(); options.Filters.Add(); }).AddApplicationPart(typeof(ShopController).Assembly).ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = _ => new ObjectResult(ApiResult.Fail("参数错误", ResultCode.ValidationError)) { StatusCode = 200 }; }); builder.Services.AddApiVersioning(options => { options.DefaultApiVersion = new ApiVersion(1, 0); options.ApiVersionReader = new UrlSegmentApiVersionReader(); }).AddMvc(); builder.Services.AddAuthentication("Test").AddScheme("Test", _ => { }); builder.Services.AddAuthorization(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddSingleton(context.Db); builder.Services.AddSingleton(context.Repository); builder.Services.AddSingleton(context); builder.Services.AddSingleton(context.Configuration); builder.Services.AddSingleton(context.Gateway); builder.Services.AddTransient(); builder.Services.AddTransient(); app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); await app.StartAsync(); client = app.GetTestClient(); client.DefaultRequestHeaders.Add("X-Test-User", "owner"); } [Theory] [InlineData("GetPddClients")] [InlineData("GetPddShopClients")] [InlineData("GetPddShopToken")] [InlineData("GetAllPddShops")] [InlineData("AddWeidianShop")] [InlineData("UpdateWeidianShop")] [InlineData("UpdateWeidianShopLogin")] [InlineData("DeleteWeidianShop")] public async Task ActionsOnlyAcceptPost(string action) { using var response = await client.GetAsync("/api/v1/shop/" + action); Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); Assert.Contains("POST", response.Content.Headers.Allow); } [Fact] public async Task ActionsBindDtoAndReturnSingleApiResultEnvelope() { using var create = await client.PostAsJsonAsync("/api/v1/shop/AddWeidianShop", new { loginName = "login", shopId = "shop", notes = "old" }); Assert.Equal(HttpStatusCode.OK, create.StatusCode); var json = await create.Content.ReadFromJsonAsync(); Assert.Equal(200, json.GetProperty("code").GetInt32()); Assert.Equal(JsonValueKind.Null, json.GetProperty("data").ValueKind); using var update = await client.PostAsJsonAsync("/api/v1/shop/UpdateWeidianShop", new { loginName = "login", shopId = "shop", notes = "new" }); Assert.Equal(200, (await update.Content.ReadFromJsonAsync()).GetProperty("code").GetInt32()); using var cookie = await client.PostAsJsonAsync("/api/v1/shop/UpdateWeidianShopLogin", new { shopId = "shop", cookie = "session" }); Assert.Equal(200, (await cookie.Content.ReadFromJsonAsync()).GetProperty("code").GetInt32()); var row = Assert.Single(await context.Db.Select().ToListAsync()); Assert.Equal("new", row.Notes); Assert.Equal("session", row.Cookie); Assert.Equal("owner", row.UserName); } [Fact] public async Task ValidationAndBusinessErrorsReturnHttp200WithFailureCode() { using var invalid = await client.PostAsJsonAsync("/api/v1/shop/AddWeidianShop", new { }); Assert.Equal(HttpStatusCode.OK, invalid.StatusCode); Assert.Equal(ResultCode.ValidationError, (await invalid.Content.ReadFromJsonAsync()).GetProperty("code").GetInt32()); using var denied = await client.PostAsJsonAsync("/api/v1/shop/GetAllPddShops", new { }); Assert.Equal(HttpStatusCode.OK, denied.StatusCode); Assert.Equal(ResultCode.BusinessError, (await denied.Content.ReadFromJsonAsync()).GetProperty("code").GetInt32()); } [Fact] public async Task CredentialResponseIsWrappedAndNotCached_AnonymousCannotRead() { using var response = await client.PostAsJsonAsync("/api/v1/shop/GetPddClients", new { }); var result = await response.Content.ReadFromJsonAsync(); Assert.Equal(200, result.GetProperty("code").GetInt32()); Assert.Equal("test-client", result.GetProperty("data")[0].GetProperty("clientId").GetString()); Assert.True(response.Headers.CacheControl!.NoStore); client.DefaultRequestHeaders.Remove("X-Test-User"); using var anonymous = await client.PostAsJsonAsync("/api/v1/shop/GetPddClients", new { }); Assert.Equal(HttpStatusCode.Unauthorized, anonymous.StatusCode); } public async Task DisposeAsync() { client.Dispose(); await app.DisposeAsync(); context.Dispose(); } } internal sealed class ShopTestAuthentication(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder) { protected override Task HandleAuthenticateAsync() { if (!Request.Headers.TryGetValue("X-Test-User", out var name)) return Task.FromResult(AuthenticateResult.NoResult()); var principal = new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.UserName, name.ToString())], Scheme.Name)); return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name))); } }