Files
StoreWeb/Tests/Store.Shops.Tests/ShopHttpTests.cs
T
2026-10-29 16:34:25 +08:00

149 lines
6.9 KiB
C#

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<GlobalExceptionFilter>();
options.Filters.Add<UnifyResultFilter>();
}).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<AuthenticationSchemeOptions, ShopTestAuthentication>("Test", _ => { });
builder.Services.AddAuthorization();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
builder.Services.AddSingleton(context.Db);
builder.Services.AddSingleton(context.Repository);
builder.Services.AddSingleton<IUserQueryService>(context);
builder.Services.AddSingleton(context.Configuration);
builder.Services.AddSingleton(context.Gateway);
builder.Services.AddTransient<ShopManagementService>();
builder.Services.AddTransient<ShopService>();
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<JsonElement>();
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<JsonElement>()).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<JsonElement>()).GetProperty("code").GetInt32());
var row = Assert.Single(await context.Db.Select<WeidianShopEntity>().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<JsonElement>()).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<JsonElement>()).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<JsonElement>();
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<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
protected override Task<AuthenticateResult> 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)));
}
}